/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.php.editor.csl;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.prefs.Preferences;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.ColoringAttributes;
import org.netbeans.modules.csl.api.OccurrencesFinder;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.Scheduler;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.php.editor.api.PhpElementKind;
import org.netbeans.modules.php.editor.lexer.LexUtilities;
import org.netbeans.modules.php.editor.lexer.PHPTokenId;
import org.netbeans.modules.php.editor.model.CodeMarker;
import org.netbeans.modules.php.editor.model.Model;
import org.netbeans.modules.php.editor.model.Occurence;
import org.netbeans.modules.php.editor.model.Occurence.Accuracy;
import org.netbeans.modules.php.editor.model.OccurencesSupport;
import org.netbeans.modules.php.editor.model.OccurrenceHighlighter;
import org.netbeans.modules.php.editor.options.MarkOccurencesSettings;
import org.netbeans.modules.php.editor.parser.PHPParseResult;

/**
 *
 * @todo Put task cancel support in reasonable places
 *
 * @author Radek Matous
 */
public class OccurrencesFinderImpl extends OccurrencesFinder {
    private Map<OffsetRange, ColoringAttributes> range2Attribs;
    private int caretPosition;
    private volatile boolean cancelled;

    @Override
    public void setCaretPosition(int position) {
        this.caretPosition = position;
    }

    @Override
    public Map<OffsetRange, ColoringAttributes> getOccurrences() {
        return range2Attribs;
    }

    @Override
    public void cancel() {
        cancelled = true;
    }

    @Override
    public void run(Result result, SchedulerEvent event) {
        //remove the last occurrences - the CSL caches the last found occurences for us
        range2Attribs = null;

        if (cancelled) {
            cancelled = false;
            return;
        }

        Preferences node = MarkOccurencesSettings.getCurrentNode();
        Map<OffsetRange, ColoringAttributes> localRange2Attribs = new HashMap<>();
        if (node.getBoolean(MarkOccurencesSettings.ON_OFF, true)) {
            for (OffsetRange r : compute((ParserResult) result, caretPosition)) {
                localRange2Attribs.put(r, ColoringAttributes.MARK_OCCURRENCES);
            }
        }

        if (cancelled) {
            cancelled = false;
            return;
        }

        if (!node.getBoolean(MarkOccurencesSettings.KEEP_MARKS, true) || localRange2Attribs.size() > 0) {
            //store the occurrences if not empty, return null in getOccurrences() otherwise
            range2Attribs = localRange2Attribs;
        }
    }

    private Collection<OffsetRange> compute(final ParserResult parameter, final int offset) {
        final PHPParseResult parseResult = (PHPParseResult) parameter;
        Set<OffsetRange> result = new TreeSet<>(new Comparator<OffsetRange>() {
            @Override
            public int compare(OffsetRange o1, OffsetRange o2) {
                return o1.compareTo(o2);
            }
        });
        final TokenHierarchy<?> tokenHierarchy = parseResult.getSnapshot().getTokenHierarchy();
        TokenSequence<PHPTokenId> tokenSequence = tokenHierarchy != null ? LexUtilities.getPHPTokenSequence(tokenHierarchy, offset) : null;
        if (cancelled) {
            return Collections.EMPTY_LIST;
        }
        OffsetRange referenceSpan = tokenSequence != null ? DeclarationFinderImpl.getReferenceSpan(tokenSequence, offset, parseResult.getModel()) : OffsetRange.NONE;
        if (!referenceSpan.equals(OffsetRange.NONE)) {
            result.addAll(getOccurrences(parseResult.getModel(), referenceSpan));
        } else {
            OccurrenceHighlighter highlighter = OccurrenceHighlighter.NONE;
            OffsetRange referenceSpanForCodeMarkers = tokenSequence != null ? getReferenceSpanForCodeMarkers(tokenSequence, offset) : OffsetRange.NONE;
            if (!referenceSpanForCodeMarkers.equals(OffsetRange.NONE)) {
                highlighter = getCodeMarkersHighlighter(parseResult.getModel(), referenceSpanForCodeMarkers);
            }
            result.addAll(highlighter.getRanges());
        }
        return result;
    }

    private Collection<OffsetRange> getOccurrences(Model model, OffsetRange referenceSpan) {
        Collection<OffsetRange> result = new TreeSet<>();
        OccurencesSupport occurencesSupport = model.getOccurencesSupport(referenceSpan);
        if (cancelled) {
            return Collections.EMPTY_LIST;
        }
        Occurence caretOccurence = occurencesSupport.getOccurence();
        if (cancelled) {
            return Collections.EMPTY_LIST;
        }
        if (caretOccurence != null) {
            final EnumSet<Accuracy> handledAccuracyFlags = EnumSet.<Occurence.Accuracy>of(
                    Accuracy.EXACT, Accuracy.EXACT_TYPE, Accuracy.MORE, Accuracy.MORE_TYPES,
                    Accuracy.UNIQUE,  Accuracy.MORE_MEMBERS);
            if (handledAccuracyFlags.contains(caretOccurence.degreeOfAccuracy())) {
                PhpElementKind kind = caretOccurence.getKind();
                if (!kind.equals(PhpElementKind.INCLUDE)) {
                    Collection<Occurence> allOccurences = caretOccurence.getAllOccurences();
                    for (Occurence occurence : allOccurences) {
                        if (handledAccuracyFlags.contains(caretOccurence.degreeOfAccuracy())) {
                            result.add(occurence.getOccurenceRange());
                        }
                    }
                }
            }
        }
        return result;
    }

    private OccurrenceHighlighter getCodeMarkersHighlighter(Model model, OffsetRange referenceSpanForCodeMarkers) {
        OccurrenceHighlighter highlighter = new OccurrenceHighlighterImpl();
        OccurencesSupport occurencesSupport = model.getOccurencesSupport(referenceSpanForCodeMarkers);
        if (cancelled) {
            return highlighter;
        }
        CodeMarker codeMarker = occurencesSupport.getCodeMarker();
        if (cancelled) {
            return highlighter;
        }
        if (codeMarker != null) {
            Collection<? extends CodeMarker> allMarkers = codeMarker.getAllMarkers();
            for (CodeMarker marker : allMarkers) {
                marker.highlight(highlighter);
            }
        }
        return highlighter;
    }

    private static OffsetRange getReferenceSpanForCodeMarkers(TokenSequence<PHPTokenId> ts, final int caretOffset) {
        ts.move(caretOffset);
        if (ts.moveNext()) {
            Token<PHPTokenId> token = ts.token();
            PHPTokenId id = token.id();
            if (id.equals(PHPTokenId.PHP_FUNCTION) || id.equals(PHPTokenId.PHP_RETURN)) {
                return new OffsetRange(ts.offset(), ts.offset() + token.length());
            }
        }
        return OffsetRange.NONE;
    }

    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public Class<? extends Scheduler> getSchedulerClass() {
        return Scheduler.CURSOR_SENSITIVE_TASK_SCHEDULER;
    }

    private static final class OccurrenceHighlighterImpl implements OccurrenceHighlighter {
        private Set<OffsetRange> offsetRanges = new TreeSet<>();

        @Override
        public void add(OffsetRange offsetRange) {
            offsetRanges.add(offsetRange);
        }

        @Override
        public Set<OffsetRange> getRanges() {
            return new TreeSet<>(offsetRanges);
        }
    }
}
