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

/**
 * A hunk3 represents a block of a change from the 3-way diff. it can either be
 * a modification on the left side (for which {@link #getLeftRange()} is not
 * {@code null}), or a change on the right side (for which
 * {@link #getRightRange()} is not {@code null}. if the left and the right
 * ranges are {@code null} this hunk represents an unmodified block of the
 * base document. if both ranges are NOT {@code null} it represents a
 * conflicting block.
 */
public class Hunk3 {

    /**
     * the left document marker pattern
     */
    public static final String[] MARKER_L = new String[] {"<<<<<<< ", ".mine"};

    /**
     * the right document marker pattern
     */
    public static final String[] MARKER_R = new String[] {">>>>>>> ", ".theirs"};

    /**
     * the base document marker pattern
     */
    public static final String[] MARKER_B = new String[] {"||||||| ", ".base"};

    /**
     * the separation marker pattern
     */
    public static final String[] MARKER_M = new String[] {"=======", ""};

    /**
     * the base range. never {@code null}.
     */
    private final Range base;

    /**
     * the left range
     */
    private final Range left;

    /**
     * the right range
     */
    private final Range right;

    /**
     * the next hunk in this chain
     */
    private Hunk3 next;

    /**
     * Constructs a new hunk and appends it to the previous one.
     * @param base the base document
     * @param left the left document
     * @param right the right document
     * @param prev the previous hunk
     */
    public Hunk3(Range base, Range left, Range right, Hunk3 prev) {
        this.base = base;
        this.left = left;
        this.right = right;
        if (prev != null) {
            prev.next = this;
        }
    }

    /**
     * Returns the next hunk in this chain or {@code null} of this hunk is
     * the last one.
     * @return the next hunk.
     */
    public Hunk3 next() {
        return next;
    }

    /**
     * Returns the range of the base document this hunk spans.
     * @return the base range.
     */
    public Range getBaseRange() {
        return base;
    }

    /**
     * Returns the range of the left document this hunk spans.
     * can be {@code null}.
     * @return the left range.
     */
    public Range getLeftRange() {
        return left;
    }

    /**
     * Returns the range of the right document this hunk spans.
     * can be {@code null}.
     * @return the right range.
     */
    public Range getRightRange() {
        return right;
    }

    /**
     * Writes this hunk to a writer. if this hunk represents a conflict it is
     * included in a merge-like manner using the separators defined above.
     *
     * @param out the writer
     * @param showBase include the block of the base document in case of a
     *        conflict as well.
     * @throws IOException if a I/O error occurs.
     */
    public void write(DiffWriter out, boolean showBase) throws IOException {
        boolean conflict = left != null && right != null;
        if (conflict) {
            out.write(getMarker(MARKER_L, left.doc));
            out.writeNewLine();
        } else {
            showBase = left == null && right == null;
        }

        if (left != null) {
            int len = Math.min(left.high, left.doc.getElements().length);
            for (int i = left.low; i < len; i++) {
                out.write(left.doc.getElements()[i].getString());
            }
        }
        if (showBase) {
            if (conflict) {
                out.write(getMarker(MARKER_B, base.doc));
                out.writeNewLine();
            }
            int len = Math.min(base.high, base.doc.getElements().length);
            for (int i = base.low; i < len; i++) {
                out.write(base.doc.getElements()[i].getString());
            }
        }
        if (conflict) {
            out.write(getMarker(MARKER_M, null));
            out.writeNewLine();
        }
        if (right != null) {
            int len = Math.min(right.high, right.doc.getElements().length);
            for (int i = right.low; i < len; i++) {
                out.write(right.doc.getElements()[i].getString());
            }
        }
        if (conflict) {
            out.write(getMarker(MARKER_R, right.doc));
            out.writeNewLine();
        }
    }

    /**
     * generate a debug string
     * @return a debug string
     */
    public String toString() {
        StringBuffer buf = new StringBuffer();
        buf.append("@@ =")
                .append(base)
                .append(" -")
                .append(left)
                .append(" +")
                .append(right)
                .append(" @@\n");
        if (left != null) {
            for (int i = 0; i < left.len(); i++) {
                addLineNumbers(buf, i);
                buf.append("< ");
                buf.append(left.doc.getElements()[i + left.low]);
            }
        }
        if (base != null) {
            for (int i = 0; i < base.len(); i++) {
                addLineNumbers(buf, i);
                buf.append("= ");
                buf.append(base.doc.getElements()[i + base.low]);
            }
        }
        if (right != null) {
            for (int i = 0; i < right.len(); i++) {
                addLineNumbers(buf, i);
                buf.append("> ");
                buf.append(right.doc.getElements()[i + right.low]);
            }
        }
        return buf.toString();
    }

    /**
     * adds some line numbers to the string buffer
     * @param buf the buffer
     * @param i the index
     */
    private void addLineNumbers(StringBuffer buf, int i) {
        buf.append("(").append(i + base.low);
        if (left != null) {
            buf.append(",").append(i + left.low);
        }
        if (right != null) {
            buf.append(",").append(i + right.low);
        }
        buf.append(") ");
    }

    /**
     * Returns the marker string for the given format and document
     * @param fmt the marker format
     * @param doc the document or {@code null}.
     * @return the marker string
     */
    public static String getMarker(String[] fmt, Document doc) {
        StringBuffer buf = new StringBuffer(fmt[0]);
        if (doc != null && doc.getSource() != null && doc.getSource().getLabel() != null) {
            buf.append(doc.getSource().getLabel());
        } else {
            buf.append(fmt[1]);
        }
        return buf.toString();
    }
}
