/*
 * 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.editor;

import javax.swing.text.Document;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.event.DocumentEvent;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.netbeans.api.editor.fold.Fold;
import org.netbeans.api.editor.fold.FoldHierarchy;
import org.netbeans.api.editor.fold.FoldType;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.spi.editor.fold.FoldHierarchyTransaction;
import org.netbeans.spi.editor.fold.FoldManager;
import org.netbeans.spi.editor.fold.FoldManagerFactory;
import org.netbeans.spi.editor.fold.FoldOperation;
import org.openide.util.RequestProcessor;

/**
 * Fold maintainer that creates and updates custom folds.
 *
 * @author Dusan Balek, Miloslav Metelka
 * @version 1.00
 */

final class CustomFoldManager implements FoldManager, Runnable {
    
    private static final Logger LOG = Logger.getLogger(CustomFoldManager.class.getName());
    
    public static final FoldType CUSTOM_FOLD_TYPE = new FoldType("custom-fold"); // NOI18N

    private FoldOperation operation;
    private Document doc;
    private org.netbeans.editor.GapObjectArray markArray = new org.netbeans.editor.GapObjectArray();
    private int minUpdateMarkOffset = Integer.MAX_VALUE;
    private int maxUpdateMarkOffset = -1;
    private List removedFoldList;
    private HashMap customFoldId = new HashMap();

    private static final RequestProcessor RP = new RequestProcessor(CustomFoldManager.class.getName(),
            1, false, false);
    private final RequestProcessor.Task task = RP.create(this);

    public void init(FoldOperation operation) {
        this.operation = operation;
        if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Initialized: {0}", System.identityHashCode(this));
        }
    }
    
    private FoldOperation getOperation() {
        return operation;
    }

    public void initFolds(FoldHierarchyTransaction transaction) {
        doc = getOperation().getHierarchy().getComponent().getDocument();
        task.schedule(300);
    }

    public void insertUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
        processRemovedFolds(transaction);
        task.schedule(300);
    }

    public void removeUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
        processRemovedFolds(transaction);
        removeAffectedMarks(evt, transaction);
        task.schedule(300);
    }
    
    public void changedUpdate(DocumentEvent evt, FoldHierarchyTransaction transaction) {
    }
    
    public void removeEmptyNotify(Fold emptyFold) {
        removeFoldNotify(emptyFold);
    }
    
    public void removeDamagedNotify(Fold damagedFold) {
        removeFoldNotify(damagedFold);
    }
    
    public void expandNotify(Fold expandedFold) {
        
    }

    public void release() {
        if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Released: {0}", System.identityHashCode(this));
        }
    }

    public void run() {
        if (operation.isReleased()) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this));
            }
            return;
        }
        ((BaseDocument) doc).readLock();
        try {
            TokenHierarchy th = TokenHierarchy.get(doc);
            if (th != null && th.isActive()) {
                FoldHierarchy hierarchy = getOperation().getHierarchy();
                hierarchy.lock();
                try {
                    if (operation.isReleased()) {
                        if (LOG.isLoggable(Level.FINE)) {
                            LOG.log(Level.FINE, "Update skipped, already relaesed: {0}", System.identityHashCode(this));
                        }
                        return;
                    }
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.log(Level.FINE, "Updating: {0}", System.identityHashCode(this));
                    }
                    FoldHierarchyTransaction transaction = getOperation().openTransaction();
                    try {
                        updateFolds(th.tokenSequence(), transaction);
                    } finally {
                        transaction.commit();
                    }
                } finally {
                    hierarchy.unlock();
                }
            }
        } finally {
            ((BaseDocument) doc).readUnlock();
        }
    }
    
    private void removeFoldNotify(Fold removedFold) {
        if (removedFoldList == null) {
            removedFoldList = new ArrayList(3);
        }
        removedFoldList.add(removedFold);
    }
    
    private void removeAffectedMarks(DocumentEvent evt, FoldHierarchyTransaction transaction) {
        int removeOffset = evt.getOffset();
        int markIndex = findMarkIndex(removeOffset);
        if (markIndex < getMarkCount()) {
            FoldMarkInfo mark;
            while (markIndex >= 0 && (mark = getMark(markIndex)).getOffset() == removeOffset) {
                mark.release(false, transaction);
                removeMark(markIndex);
                markIndex--;
            }
        }
    }
    
    private void processRemovedFolds(FoldHierarchyTransaction transaction) {
        if (removedFoldList != null) {
            for (int i = removedFoldList.size() - 1; i >= 0; i--) {
                Fold removedFold = (Fold)removedFoldList.get(i);
                FoldMarkInfo startMark = (FoldMarkInfo)getOperation().getExtraInfo(removedFold);
                if (startMark.getId() != null)
                    customFoldId.put(startMark.getId(), Boolean.valueOf(removedFold.isCollapsed())); // remember the last fold's state before remove
                FoldMarkInfo endMark = startMark.getPairMark(); // get prior releasing
                if (getOperation().isStartDamaged(removedFold)) { // start mark area was damaged
                    startMark.release(true, transaction); // forced remove
                }
                if (getOperation().isEndDamaged(removedFold)) {
                    endMark.release(true, transaction);
                }
            }
        }
        removedFoldList = null;
    }

    private void markUpdate(FoldMarkInfo mark) {
        markUpdate(mark.getOffset());
    }
    
    private void markUpdate(int offset) {
        if (offset < minUpdateMarkOffset) {
            minUpdateMarkOffset = offset;
        }
        if (offset > maxUpdateMarkOffset) {
            maxUpdateMarkOffset = offset;
        }
    }
    
    private FoldMarkInfo getMark(int index) {
        return (FoldMarkInfo)markArray.getItem(index);
    }
    
    private int getMarkCount() {
        return markArray.getItemCount();
    }
    
    private void removeMark(int index) {
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Removing mark from ind=" + index + ": " + getMark(index)); // NOI18N
        }
        markArray.remove(index, 1);
    }
    
    private void insertMark(int index, FoldMarkInfo mark) {
        markArray.insertItem(index, mark);
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Inserted mark at ind=" + index + ": " + mark); // NOI18N
        }
    }

    private int findMarkIndex(int offset) {
        int markCount = getMarkCount();
        int low = 0;
        int high = markCount - 1;
        
        while (low <= high) {
            int mid = (low + high) / 2;
            int midMarkOffset = getMark(mid).getOffset();
            
            if (midMarkOffset < offset) {
                low = mid + 1;
            } else if (midMarkOffset > offset) {
                high = mid - 1;
            } else {
                // mark starting exactly at the given offset found
                // If multiple -> find the one with highest index
                mid++;
                while (mid < markCount && getMark(mid).getOffset() == offset) {
                    mid++;
                }
                mid--;
                return mid;
            }
        }
        return low; // return higher index (e.g. for insert)
    }
    
    private List<FoldMarkInfo> getMarkList(TokenSequence seq) {
        List<FoldMarkInfo> markList = null;
        
        for(seq.moveStart(); seq.moveNext(); ) {
            Token token = seq.token();
            FoldMarkInfo info;
            try {
                info = scanToken(token);
            } catch (BadLocationException e) {
                LOG.log(Level.WARNING, null, e);
                info = null;
            }

            if (info != null) {
                if (markList == null) {
                    markList = new ArrayList<FoldMarkInfo>();
                }
                markList.add(info);
            }
        }

        return markList;
    }
    
    private void processTokenList(TokenSequence seq, FoldHierarchyTransaction transaction) {
        List<FoldMarkInfo> markList = getMarkList(seq);
        int markListSize;
        if (markList != null && ((markListSize = markList.size()) > 0)) {
            // Find the index for insertion
            int offset = ((FoldMarkInfo)markList.get(0)).getOffset();
            int arrayMarkIndex = findMarkIndex(offset);
            // Remember the corresponding mark in the array as well
            FoldMarkInfo arrayMark;
            int arrayMarkOffset;
            if (arrayMarkIndex < getMarkCount()) {
                arrayMark = getMark(arrayMarkIndex);
                arrayMarkOffset = arrayMark.getOffset();
            } else { // at last mark
                arrayMark = null;
                arrayMarkOffset = Integer.MAX_VALUE;
            }

            for (int i = 0; i < markListSize; i++) {
                FoldMarkInfo listMark = (FoldMarkInfo)markList.get(i);
                int listMarkOffset = listMark.getOffset();
                if (i == 0 || i == markListSize - 1) {
                    // Update the update-offsets by the first and last marks in the list
                    markUpdate(listMarkOffset);
                }
                while (listMarkOffset >= arrayMarkOffset) {
                    if (listMarkOffset == arrayMarkOffset) {
                        // At the same offset - likely the same mark
                        //   -> retain the collapsed state
                        listMark.setCollapsed(arrayMark.isCollapsed());
                    }
                    if (!arrayMark.isReleased()) { // make sure that the mark is released
                        arrayMark.release(false, transaction); 
                    }
                    removeMark(arrayMarkIndex);
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.fine("Removed dup mark from ind=" + arrayMarkIndex + ": " + arrayMark); // NOI18N
                    }
                    if (arrayMarkIndex < getMarkCount()) {
                        arrayMark = getMark(arrayMarkIndex);
                        arrayMarkOffset = arrayMark.getOffset();
                    } else { // no more marks
                        arrayMark = null;
                        arrayMarkOffset = Integer.MAX_VALUE;
                    }
                }
                // Insert the listmark
                insertMark(arrayMarkIndex, listMark);
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Inserted mark at ind=" + arrayMarkIndex + ": " + listMark); // NOI18N
                }
                arrayMarkIndex++;
            }
        }
    }

    private void updateFolds(TokenSequence seq, FoldHierarchyTransaction transaction) {

        if (seq != null && !seq.isEmpty()) {
            processTokenList(seq, transaction);
        }

        if (maxUpdateMarkOffset == -1) { // no updates
            return;
        }
        
        // Find the first mark to update and init the prevMark and parentMark prior the loop
        int index = findMarkIndex(minUpdateMarkOffset);
        FoldMarkInfo prevMark;
        FoldMarkInfo parentMark;
        if (index == 0) { // start from begining
            prevMark = null;
            parentMark = null;
        } else {
            prevMark = getMark(index - 1);
            parentMark = prevMark.getParentMark();
        }
        
        // Iterate through the changed marks in the mark array 
        int markCount = getMarkCount();
        while (index < markCount) { // process the marks
            FoldMarkInfo mark = getMark(index);

            // If the mark was released then it must be removed
            if (mark.isReleased()) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Removing released mark at ind=" + index + ": " + mark); // NOI18N
                }
                removeMark(index);
                markCount--;
                continue;
            }

            // Update mark's status (folds, parentMark etc.)
            if (mark.isStartMark()) { // starting a new fold
                if (prevMark == null || prevMark.isStartMark()) { // new level
                    mark.setParentMark(prevMark); // prevMark == null means root level
                    parentMark = prevMark;

                } // same level => parent to the parent of the prevMark

            } else { // end mark
                if (prevMark != null) {
                    if (prevMark.isStartMark()) { // closing nearest fold
                        prevMark.setEndMark(mark, false, transaction);

                    } else { // prevMark is end mark - closing its parent fold
                        if (parentMark != null) {
                            // mark's parent gets set as well
                            parentMark.setEndMark(mark, false, transaction);
                            parentMark = parentMark.getParentMark();

                        } else { // prevMark's parentMark is null (top level)
                            mark.makeSolitaire(false, transaction);
                        }
                    }
                    
                } else { // prevMark is null
                    mark.makeSolitaire(false, transaction);
                }
            }

            // Set parent mark of the mark
            mark.setParentMark(parentMark);

            
            prevMark = mark;
            index++;
        }

        minUpdateMarkOffset = Integer.MAX_VALUE;
        maxUpdateMarkOffset = -1;
        
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("MARKS DUMP:\n" + this); //NOI18N
        }
    }
    
    public @Override String toString() {
        StringBuffer sb = new StringBuffer();
        int markCount = getMarkCount();
        int markCountDigitCount = Integer.toString(markCount).length();
        for (int i = 0; i < markCount; i++) {
            sb.append("["); // NOI18N
            String iStr = Integer.toString(i);
            appendSpaces(sb, markCountDigitCount - iStr.length());
            sb.append(iStr);
            sb.append("]:"); // NOI18N
            FoldMarkInfo mark = getMark(i);
            
            // Add extra indent regarding the depth in hierarchy
            int indent = 0;
            FoldMarkInfo parentMark = mark.getParentMark();
            while (parentMark != null) {
                indent += 4;
                parentMark = parentMark.getParentMark();
            }
            appendSpaces(sb, indent);

            sb.append(mark);
            sb.append('\n');
        }
        return sb.toString();
    }
    
    private static void appendSpaces(StringBuffer sb, int spaces) {
        while (--spaces >= 0) {
            sb.append(' ');
        }
    }

    private static Pattern pattern = Pattern.compile(
            "(<\\s*editor-fold" +
            // id="x"[opt] defaultstate="y"[opt] desc="z"[opt] defaultstate="a"[opt]
            // id must be first, the rest of attributes in random order
            "(?:(?:\\s+id=\"(\\S*)\")?(?:\\s+defaultstate=\"(\\S*?)\")?(?:\\s+desc=\"([\\S \\t]*?)\")?(?:\\s+defaultstate=\"(\\S*?)\")?)" +
            "\\s*>)|(?:</\\s*editor-fold\\s*>)"); // NOI18N

    private FoldMarkInfo scanToken(Token token) throws BadLocationException {
        // ignore any token that is not comment
        if (token.id().primaryCategory() != null && token.id().primaryCategory().startsWith("comment")) { //NOI18N
            Matcher matcher = pattern.matcher(token.text());
            if (matcher.find()) {
                if (matcher.group(1) != null) { // fold's start mark found
                    boolean state;
                    if (matcher.group(3) != null) {
                        state = "collapsed".equals(matcher.group(3)); // remember the defaultstate // NOI18N
                    } else {
                        state = "collapsed".equals(matcher.group(5));
                    }
                    
                    if (matcher.group(2) != null) { // fold's id exists
                        Boolean collapsed = (Boolean)customFoldId.get(matcher.group(2));
                        if (collapsed != null)
                            state = collapsed.booleanValue(); // fold's state is already known from the past
                        else
                            customFoldId.put(matcher.group(2), Boolean.valueOf(state));
                    }
                    return new FoldMarkInfo(true, token.offset(null), matcher.end(0), matcher.group(2), state, matcher.group(4)); // NOI18N
                } else { // fold's end mark found
                    return new FoldMarkInfo(false, token.offset(null), matcher.end(0), null, false, null);
                }
            }
        }
        return null;
    }

    private final class FoldMarkInfo {

        private boolean startMark;
        private Position pos;
        private int length;
        private String id;
        private boolean collapsed;
        private String description;

        /** Matching pair mark used for fold construction */
        private FoldMarkInfo pairMark;
        
        /** Parent mark defining nesting in the mark hierarchy. */
        private FoldMarkInfo parentMark;
        
        /**
         * Fold that corresponds to this mark (if it's start mark).
         * It can be null if this mark is end mark or if it currently
         * does not have the fold assigned.
         */
        private Fold fold;
        
        private boolean released;
        
        private FoldMarkInfo(boolean startMark, int offset,
                             int length, String id, boolean collapsed, String description)
        throws BadLocationException {

            this.startMark = startMark;
            this.pos = doc.createPosition(offset);
            this.length = length;
            this.id = id;
            this.collapsed = collapsed;
            this.description = description;
        }

        public String getId() {
            return id;
        }

        public String getDescription() {
            return description;
        }

        public boolean isStartMark() {
            return startMark;
        }

        public int getLength() {
            return length;
        }

        public int getOffset() {
            return pos.getOffset();
        }
        
        public int getEndOffset() {
            return getOffset() + getLength();
        }

        public boolean isCollapsed() {
            return (fold != null) ? fold.isCollapsed() : collapsed;
        }
        
        public boolean hasFold() {
            return (fold != null);
        }
        
        public void setCollapsed(boolean collapsed) {
            this.collapsed = collapsed;
        }
        
        public boolean isSolitaire() {
            return (pairMark == null);
        }
        
        public void makeSolitaire(boolean forced, FoldHierarchyTransaction transaction) {
            if (!isSolitaire()) {
                if (isStartMark()) {
                    setEndMark(null, forced, transaction);
                } else { // end mark
                    getPairMark().setEndMark(null, forced, transaction);
                }
            }
        }
        
        public boolean isReleased() {
            return released;
        }
        
        /**
         * Release this mark and mark for update.
         */
        public void release(boolean forced, FoldHierarchyTransaction transaction) {
            if (!released) {
                makeSolitaire(forced, transaction);
                released = true;
                markUpdate(this);
            }
        }
        
        public FoldMarkInfo getPairMark() {
            return pairMark;
        }
        
        private void setPairMark(FoldMarkInfo pairMark) {
            this.pairMark = pairMark;
        }

        public void setEndMark(FoldMarkInfo endMark, boolean forced,
        FoldHierarchyTransaction transaction) {
            if (!isStartMark()) {
                throw new IllegalStateException("Not start mark"); // NOI18N
            }
            if (pairMark == endMark) {
                return;
            }
            
            if (pairMark != null) { // is currently paired to an end mark
                releaseFold(forced, transaction);
                pairMark.setPairMark(null);
            }

            pairMark = endMark;
            if (endMark != null) {
                if (!endMark.isSolitaire()) { // make solitaire first
                    endMark.makeSolitaire(false, transaction); // not forced here
                }
                endMark.setPairMark(this);
                endMark.setParentMark(this.getParentMark());
                ensureFoldExists(transaction);
            }
        }
        
        public FoldMarkInfo getParentMark() {
            return parentMark;
        }
        
        public void setParentMark(FoldMarkInfo parentMark) {
            this.parentMark = parentMark;
        }
        
        private void releaseFold(boolean forced, FoldHierarchyTransaction transaction) {
            if (isSolitaire() || !isStartMark()) {
               throw new IllegalStateException();
            }

            if (fold != null) {
                setCollapsed(fold.isCollapsed()); // serialize the collapsed info
                if (!forced) {
                    getOperation().removeFromHierarchy(fold, transaction);
                }
                fold = null;
            }
        }

        public Fold getFold() {
            if (isSolitaire()) {
                return null;
            }
            if (!isStartMark()) {
                return pairMark.getFold();
            }
            return fold;
        }
        
        public void ensureFoldExists(FoldHierarchyTransaction transaction) {
            if (isSolitaire() || !isStartMark()) {
                throw new IllegalStateException();
            }

            if (fold == null) {
                try {
                    if (!startMark) {
                        throw new IllegalStateException("Not start mark: " + this); // NOI18N
                    }
                    if (pairMark == null) {
                        throw new IllegalStateException("No pairMark for mark:" + this); // NOI18N
                    }
                    int startOffset = getOffset();
                    int startGuardedLength = getLength();
                    int endGuardedLength = pairMark.getLength();
                    int endOffset = pairMark.getOffset() + endGuardedLength;
                    fold = getOperation().addToHierarchy(
                        CUSTOM_FOLD_TYPE, getDescription(), collapsed,
                        startOffset, endOffset,
                        startGuardedLength, endGuardedLength,
                        this,
                        transaction
                    );
                } catch (BadLocationException e) {
                    LOG.log(Level.WARNING, null, e);
                }
            }
        }
        
        public @Override String toString() {
            StringBuffer sb = new StringBuffer();
            sb.append(isStartMark() ? 'S' : 'E');  // NOI18N
            
            // Check whether this mark (or its pair) has fold
            if (hasFold() || (!isSolitaire() && getPairMark().hasFold())) {
                sb.append("F"); // NOI18N
                
                // Check fold's status
                if (isStartMark() && (isSolitaire()
                        || getOffset() != fold.getStartOffset()
                        || getPairMark().getEndOffset() != fold.getEndOffset())
                ) {
                    sb.append("!!<"); // NOI18N
                    sb.append(fold.getStartOffset());
                    sb.append(","); // NOI18N
                    sb.append(fold.getEndOffset());
                    sb.append(">!!"); // NOI18N
                }
            }

            // Append mark's internal status
            sb.append(" ("); // NOI18N
            sb.append("o="); // NOI18N
            sb.append(pos.getOffset());
            sb.append(", l="); // NOI18N
            sb.append(length);
            sb.append(", d='"); // NOI18N
            sb.append(description);
            sb.append('\'');
            if (getPairMark() != null) {
                sb.append(", <->"); // NOI18N
                sb.append(getPairMark().getOffset());
            }
            if (getParentMark() != null) {
                sb.append(", ^"); // NOI18N
                sb.append(getParentMark().getOffset());
            }
            sb.append(')');
            
            return sb.toString();
        }

    }
        
    public static final class Factory implements FoldManagerFactory {

        public FoldManager createFoldManager() {
            return new CustomFoldManager();
        }
    }
}