/*
 * 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.apache.jackrabbit.vault.util.diff;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;

/**
 * The document diff is used to create a <em>diff</em> between 2 documents.
 * It provides 2 possibilites to use the differences: either using the
 * {@link ChangeListener} which is called for each line of the document, or
 * using the {@link Hunk}s that form the differences of this diff.
 */
public class DocumentDiff {

    /**
     * the left document
     */
    private Document left;

    /**
     * the right document
     */
    private Document right;

    /**
     * the change script
     */
    private final Diff.Change change;

    /**
     * the hunks
     */
    private Hunk hunks;

    /**
     * the number of changes
     */
    private int numDelta = 0;

    /**
     * Create a new diff from the given 2 documents
     * @param left the left document
     * @param right the right document
     */
    public DocumentDiff(Document left, Document right) {
        this.left = left;
        this.right = right;
        Document.Element[] leftElems = left.getElements();
        Document.Element[] rightElems = right.getElements();
        change = new Diff(leftElems, rightElems).diff_2(false);
        init();
    }

    /**
     * Returns the number of changed elements. each insertion and each deletion
     * counts as 1 change. if elements were modified the greate change is counted.
     * eg: if you change 'foo' to 'bar' this is actually 1 deletion and 1 insertion
     * but counts as 1 change. if you change 'foo\nbar\n' to 'hello' this counts
     * as 2 changes since this includes 2 deletions.
     *
     * @return the number of changed elements.
     */
    public int getNumDeltaElements() {
        return numDelta;
    }

    /**
     * Returns the linked list of hunks
     * @return the hunks.
     */
    public Hunk getHunks() {
        return hunks;
    }

    /**
     * Same as {@link #write(Writer, int)} but to a string buffer.
     *
     * @param buf the buffer
     * @param numContextLines the number of context lines.
     */
    public void write(StringBuffer buf, int numContextLines) {
        try {
            StringWriter out = new StringWriter();
            write(new DiffWriter(out), numContextLines);
            out.close();
            buf.append(out.getBuffer());
        } catch (IOException e) {
            throw new IllegalStateException(e.toString());
        }
    }

    /**
     * Same as {@link #write(DiffWriter, int)} but to a string buffer.
     *
     * @param buf the buffer
     * @param lineSeparator the line separator to use
     * @param numContextLines the number of context lines.
     */
    public void write(StringBuffer buf, String lineSeparator, int numContextLines) {
        try {
            StringWriter out = new StringWriter();
            write(new DiffWriter(out, lineSeparator), numContextLines);
            out.close();
            buf.append(out.getBuffer());
        } catch (IOException e) {
            throw new IllegalStateException(e.toString());
        }
    }

    /**
     * Same as {@link #write(DiffWriter, int)} but wraps the given writer
     * with a default diff writer.
     *
     * @param out the writer
     * @param numContextLines the number of context lines.
     * @throws IOException if an I/O error occurs
     */
    public void write(Writer out, int numContextLines) throws IOException {
        DiffWriter dw = new DiffWriter(out);
        write(dw, numContextLines);
        dw.flush();
    }

    /**
     * Writes the differences to the given writer in a unified diff
     * format. the context lines specify how many unmodified lines should
     * sourround the actual difference.
     *
     * @param out the writer
     * @param numContextLines the number of context lines.
     * @throws IOException if an I/O error occurs
     */
    public void write(DiffWriter out, int numContextLines) throws IOException {
        if (hunks != null) {
            if (left.getSource() != null && right.getSource() != null) {
                out.write("--- ");
                out.write(left.getSource().getLocation());
                out.writeNewLine();
                out.write("+++ ");
                out.write(right.getSource().getLocation());
                out.writeNewLine();
            } else {
                out.write("--- .mine");
                out.writeNewLine();
                out.write("+++ .theirs");
                out.writeNewLine();
            }
            Hunk hunk = hunks;
            while (hunk != null) {
                hunk = hunk.write(out, numContextLines);
            }
        }
    }

    /**
     * init the hunks and the delta counter.
     */
    private void init() {
        Diff.Change c = change;
        int leftPos = 0;
        int rightPos = 0;
        Hunk first = new Hunk(null, null, 0, null);
        Hunk hunk = first;
        while (c != null) {
            numDelta += Math.max(c.deleted, c.inserted);
            if (leftPos < c.line0) {
                // add unmodified hunk
                int len = c.line0 - leftPos;
                hunk = new Hunk(
                        new Range(left, leftPos, leftPos + len),
                        new Range(right, rightPos, rightPos + len),
                        Hunk.UNMODIFIED,
                        hunk);
                leftPos+= len;
                rightPos+= len;
            }
            // add hunk
            hunk = new Hunk(
                    new Range(left, leftPos, leftPos + c.deleted),
                    new Range(right, rightPos, rightPos + c.inserted),
                    (c.deleted > 0 ? Hunk.DELETED : 0)|
                    (c.inserted > 0 ? Hunk.INSERTED : 0),
                    hunk);
            leftPos+=c.deleted;
            rightPos+=c.inserted;
            c = c.nextChange;
        }
        // last hunk
        if (leftPos < left.getElements().length) {
            int len = left.getElements().length - leftPos;
            new Hunk(
                    new Range(left, leftPos, leftPos + len),
                    new Range(right, rightPos, rightPos + len),
                    Hunk.UNMODIFIED,
                    hunk);
        }
        // and record the first valid hunk
        hunks = first.next();
    }

    /**
     * Iterate over all changes and invoke the respective methods in the given
     * listener. the context lines specify how many unmodified lines should
     * sourround the respective change.
     *
     * @param listener the change listener
     * @param numContextLines the number of context lines
     */
    public void showChanges(ChangeListener listener, int numContextLines) {
        Diff.Change c = change;
        Document.Element[] lines0 = left.getElements();
        Document.Element[] lines1 = right.getElements();
        listener.onDocumentsStart(left, right);
        while (c != null) {
            Diff.Change first = c;
            int start0 = Math.max(c.line0 - numContextLines, 0);
            int end0 = Math.min(c.line0 + c.deleted + numContextLines, lines0.length);
            int start1 = Math.max(c.line1 - numContextLines, 0);
            int end1 = Math.min(c.line1 + c.inserted + numContextLines, lines1.length);
            while (c != null) {
                if (c.line0 <= end0) {
                    end0 = Math.min(c.line0 + c.deleted + numContextLines, lines0.length);
                    end1 = Math.min(c.line1 + c.inserted + numContextLines, lines1.length);
                    c = c.nextChange;
                } else {
                    break;
                }
            }
            listener.onChangeStart(start0, end0 - start0 - 1, start1, end1 - start1 - 1);
            //dump(fmt, first, c, numContextLines);
            while (first != c) {
                while (start0 < first.line0) {
                    listener.onUnmodified(start0, start1, lines0[start0]);
                    start0++;
                    start1++;
                }
                for (int i = 0; i < first.deleted; i++) {
                    listener.onDeleted(start0, first.line1, lines0[start0]);
                    start0++;
                }
                for (int i = 0; i < first.inserted; i++) {
                    listener.onInserted(first.line0, start1, lines1[start1]);
                    start1++;
                }
                first = first.nextChange;
            }
            for (int i = 0; i < numContextLines && start0 < lines0.length; i++) {
                listener.onUnmodified(start0, start1, lines0[start0]);
                start0++;
                start1++;
            }
            listener.onChangeEnd();
        }
        listener.onDocumentsEnd(left, right);
    }

    /**
     * Returns an element factory that provides the elements of the merged
     * result of this diff where the left document is dominant.
     * @return the merged elements.
     */
    public ElementsFactory getMergedLeft() {
        return new MergeElementFactory(true);
    }

    /**
     * Returns an element factory that provides the elements of the merged
     * result of this diff where the right document is dominant.
     * @return the merged elements.
     */
    public ElementsFactory getMergedRight() {
        return new MergeElementFactory(false);
    }

    /**
     * element factory that provides the merged result.
     */
    private class MergeElementFactory implements ElementsFactory {

        /**
         * if {@code true}, do the merge reverse
         */
        boolean reverse;

        /**
         * Create a new element factory.
         * @param reverse if {@code true}, do the merge revese
         */
        public MergeElementFactory(boolean reverse) {
            this.reverse = reverse;
        }

        /**
         * {@inheritDoc}
         */
        public Document.Element[] getElements() {
            Diff.Change c = change;
            Document.Element[] lines0 = left.getElements();
            Document.Element[] lines1 = right.getElements();
            ArrayList elems = new ArrayList();
            while (c != null) {
                Diff.Change first = c;
                int start0 = 0;
                int end0 = lines0.length;
                int start1 = 0;
                int end1 = lines1.length;
                while (c != null) {
                    if (c.line0 <= end0) {
                        end0 = lines0.length;
                        end1 = lines1.length;
                        c = c.nextChange;
                    } else {
                        break;
                    }
                }
                while (first != c) {
                    while (start0 < first.line0) {
                        elems.add(lines0[start0]);
                        start0++;
                        start1++;
                    }
                    for (int i = 0; i < first.deleted; i++) {
                        if (reverse) {
                            elems.add(lines0[start0]);
                        }
                        start0++;
                    }
                    for (int i = 0; i < first.inserted; i++) {
                        if (!reverse) {
                            elems.add(lines1[start1]);
                        }
                        start1++;
                    }
                    first = first.nextChange;
                }
                while (start0 < lines0.length) {
                    elems.add(lines0[start0]);
                    start0++;
                    start1++;
                }
            }
            return (Document.Element[]) elems.toArray(new Document.Element[elems.size()]);
        }
    }

}
