/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core.internal.util;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.*;

import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.InheritedProperties;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @author TMate Software Ltd.
 * @version 1.3
 */
public class SVNSkel {

    private static final int DEFAULT_BUFFER_SIZE = 1024;

    public static final char TYPE_NOTHING = 0;
    public static final char TYPE_SPACE = 1;
    public static final char TYPE_DIGIT = 2;
    public static final char TYPE_PAREN = 3;
    public static final char TYPE_NAME = 4;

    private static final char[] TYPES_TABLE = new char[]{
            0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            1, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0,
            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0,

            /* 64 */
            0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 0, 3, 0, 0,
            0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0,

            /* 128 */
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

            /* 192 */
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    };

    public static char getType(byte b) {
        return TYPES_TABLE[b & 0xFF];
    }

    public static SVNSkel parse(byte[] data) throws SVNException {
        if (data == null) {
            return null;
        }
        return parse(data, 0, data.length);
    }

    public static SVNSkel parse(byte[] data, int offset, int length) throws SVNException {
        if (data == null || length == 0 || offset + length > data.length) {
            return null;
        }
        ByteBuffer buffer = ByteBuffer.wrap(data, offset, length);
        return parse(buffer);
    }

    public static SVNSkel parse(ByteBuffer buffer) throws SVNException {
        if (buffer == null || !buffer.hasRemaining()) {
            return null;
        }
        byte cur = buffer.get(buffer.position());
        if (cur == '(') {
            return parseList(buffer);
        }
        if (getType(cur) == TYPE_NAME) {
            return parseImplicitAtom(buffer);
        }
        return parseExplicitAtom(buffer);
    }

    public static SVNSkel parseList(ByteBuffer buffer) throws SVNException {
        if (buffer == null || !buffer.hasRemaining()) {
            return null;
        }
        if (buffer.get() != '(') {
            return null;
        }
        if (!buffer.hasRemaining()) {
            return null;
        }
        SVNSkel list = createEmptyList();
        while (true) {
            byte cur = 0;
            while (buffer.hasRemaining()) {
                cur = buffer.get();
                if (getType(cur) != TYPE_SPACE) {
                    break;
                }
            }
            if (cur == ')') {
                break;
            }
            if (!buffer.hasRemaining()) {
                //cur is not ')' and the buffer has no more character, SVNSkel has incorrect format
                return null;
            }
            buffer = unread(buffer, 1);
            SVNSkel element = parse(buffer);
            if (element == null) {
                return null;
            }
            list.appendChild(element);
        }
        return list;
    }

    public static SVNSkel parseImplicitAtom(ByteBuffer buffer) {
        if (buffer == null || !buffer.hasRemaining()) {
            return null;
        }
        if (getType(buffer.get(buffer.position())) != TYPE_NAME) {
            return null;
        }
        int start = buffer.position();
        while (buffer.hasRemaining()) {
            byte cur = buffer.get();
            if (getType(cur) == TYPE_SPACE || getType(cur) == TYPE_PAREN) {
                buffer = unread(buffer, 1);
                break;
            }
        }
        return createAtom(buffer.array(), buffer.arrayOffset() + start, buffer.position() - start);
    }

    public static SVNSkel parseExplicitAtom(ByteBuffer buffer) {
        if (buffer == null || !buffer.hasRemaining()) {
            return null;
        }
        int size = parseSize(buffer, buffer.remaining());
        if (size < 0) {
            return null;
        }
        if (!buffer.hasRemaining() || getType(buffer.get()) != TYPE_SPACE) {
            return null;
        }
        int start = buffer.arrayOffset() + buffer.position();
        buffer.position(buffer.position() + size);
        return createAtom(buffer.array(), start, size);
    }

    private static SVNSkel createAtom(SVNPropertyValue propertyValue) {
        if (propertyValue != null && propertyValue.getString() != null) {
            return createAtom(propertyValue.getString());
        } else if (propertyValue != null && propertyValue.getBytes() != null) {
            return createAtom(propertyValue.getBytes());
        } else {
            return createAtom("");
        }
    }

    public static SVNSkel createAtom(String str) {
        if (str == null) {
            return null;
        }
        byte[] data;
        try {
            data = str.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            data = str.getBytes();
        }
        return new SVNSkel(data);
    }

    public static SVNSkel createAtom(byte[] data) {
        if (data == null) {
            return null;
        }
        return createAtom(data, 0, data.length);
    }

    public static SVNSkel createAtom(byte[] data, int offset, int length) {
        if (data == null) {
            return null;
        }
        byte[] raw = new byte[length];
        System.arraycopy(data, offset, raw, 0, length);
        return new SVNSkel(raw);
    }

    public static SVNSkel createEmptyList() {
        return new SVNSkel();
    }

    public static SVNSkel createPropList(Map<String, SVNPropertyValue> props) throws SVNException {
        SVNSkel list = createEmptyList();
        if (props == null) {
            return list;
        }
        for(String propertyName : props.keySet()) {
            SVNSkel name = createAtom(propertyName);
            SVNPropertyValue pv = props.get(propertyName);
            SVNSkel value = createAtom(pv);

            list.prepend(value);
            list.prepend(name);
            
        }
        if (!list.isValidPropList()) {
            error("proplist");
        }
        return list;
    }

    final private byte[] myRawData;
    final private List<SVNSkel> myList;
    private SVNSkel myNext;

    protected SVNSkel(byte[] data) {
        myRawData = data;
        myList = null;
        myNext = null;
    }

    protected SVNSkel() {
        myRawData = null;
        myList = new ArrayList<SVNSkel>();
    }

    public boolean isAtom() {
        return myList == null;
    }

    public byte[] getData() {
        return myRawData;
    }

    public List<SVNSkel> getList() {
        return Collections.unmodifiableList(myList);
    }

    public SVNSkel first() {
        if (myList != null && !myList.isEmpty()) {
            return myList.get(0);
        }
        return null;
    }
    
    public SVNSkel next() {
        return myNext;
    }

    public SVNSkel getChild(int i) throws SVNException {
        if (isAtom()) {
            SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "Unable to get a child from atom");
            SVNErrorManager.error(error, SVNLogType.DEFAULT);
        }
        return (SVNSkel) myList.get(i);
    }

    public void appendChild(SVNSkel child) throws SVNException {
        if (isAtom()) {
            SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "Unable to add a child to atom");
            SVNErrorManager.error(error, SVNLogType.DEFAULT);
        }
        if (!myList.isEmpty()) {
            myList.get(myList.size() - 1).myNext = child;
        }
        myList.add(child);
    }

    public void prepend(SVNSkel child) throws SVNException {
        if (isAtom()) {
            SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "Unable to add a child to atom");
            SVNErrorManager.error(error, SVNLogType.DEFAULT);
        }
        if (!myList.isEmpty()) {
            child.myNext = myList.get(0);
        }
        myList.add(0, child);
    }

    public void prependString(String str) throws SVNException {
        SVNSkel skel = SVNSkel.createAtom(str);
        prepend(skel);
    }

    public void prependPropertyValue(SVNPropertyValue propertyValue) throws SVNException{
        SVNSkel skel = SVNSkel.createAtom(propertyValue);
        prepend(skel);
    }

    public void prependPath(File path) throws SVNException {        
        String str = path != null ? SVNFileUtil.getFilePath(path) : "";
        prependString(str);
    }

    public int getListSize() {
        if (isAtom()) {
            return -1;
        }
        return myList.size();
    }

    public String getValue() {
        if (isAtom()) {
            String str;
            try {
                str = new String(getData(), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                str = new String(getData());
            }
            return str;
        }
        return null;
    }

    public String toString() {
        StringBuffer buffer = new StringBuffer();
        if (isAtom()) {
            buffer.append("[");
            buffer.append(getValue());
            buffer.append("]");
        } else {
            buffer.append("(");
            for (Iterator<SVNSkel> iterator = myList.iterator(); iterator.hasNext();) {
                SVNSkel element = (SVNSkel) iterator.next();
                buffer.append(element.toString());
            }
            buffer.append(")");
        }
        return buffer.toString();
    }

    public boolean contentEquals(String str) {
        if (!isAtom()) {
            return false;
        }
        String value = getValue();
        return value.equals(str);
    }

    public boolean containsAtomsOnly() {
        if (isAtom()) {
            return false;
        }
        for (Iterator<SVNSkel> iterator = myList.iterator(); iterator.hasNext();) {
            SVNSkel element = (SVNSkel) iterator.next();
            if (!element.isAtom()) {
                return false;
            }
        }
        return true;
    }

    public boolean isValidPropList() {
        int length = getListSize();
        if (length >= 0 && (length & 1) == 0) {
            return containsAtomsOnly();
        }
        return false;
    }

    public Map<String, byte[]> parsePropList() throws SVNException {
        if (!isValidPropList()) {
            error("proplist");
        }
        @SuppressWarnings("unchecked")
        Map<String, byte[]> props = new SVNHashMap();
        for (Iterator<SVNSkel> iterator = myList.iterator(); iterator.hasNext();) {
// We always have name - value pair since list length is even
            SVNSkel nameElement = (SVNSkel) iterator.next();
            SVNSkel valueElement = (SVNSkel) iterator.next();
            String name = nameElement.getValue();
            byte[] value = valueElement.isAtom() ? valueElement.getData() : null;
            props.put(name, value);
        }
        return props;
    }

    public boolean isValidInheritedProperties() {
        final int length = getListSize();
        if (length >= 0 && (length & 1) == 0) {
            for(SVNSkel child = first(); child != null; child = child.next()) {
                if (!child.isAtom()) {
                    return false;
                }
                if (child.next() == null) {
                    return false;
                }
                child = child.next();
                if (!child.isValidPropList()) {
                    return false;
                }
            }
        }
        return true;
    }

    public static SVNSkel createInheritedProperties(Map<String, SVNProperties> iprops) throws SVNException {
        SVNSkel result = createEmptyList();
        for (String path : iprops.keySet()) {
            SVNSkel pathSkel = SVNSkel.createAtom(path);
            SVNSkel propSkel = SVNSkel.createPropList(iprops.get(path).asMap());
            result.appendChild(pathSkel);
            result.appendChild(propSkel);
        }
        return result;
    }

    public List<Structure<InheritedProperties>> parseInheritedProperties() throws SVNException {
        if (!isValidInheritedProperties()) {
            error("iprops");
        }
        List<Structure<InheritedProperties>> result = new ArrayList<Structure<InheritedProperties>>();
        for(SVNSkel elt = first(); elt != null; elt = elt.next()) {
            final String parent = elt.getValue();
            elt = elt.next();
            final SVNProperties props = SVNProperties.wrap(elt.parsePropList());
            final Structure<InheritedProperties> element = Structure.obtain(InheritedProperties.class);
            element.set(InheritedProperties.pathOrURL, parent);
            element.set(InheritedProperties.properties, props);
            result.add(element);
        }
        return result;
    }

    public byte[] unparse() throws SVNException {
        int approxSize = estimateUnparsedSize();
        ByteBuffer buffer = ByteBuffer.allocate(approxSize);
        buffer = writeTo(buffer);
        buffer.flip();
        byte[] raw = new byte[buffer.limit() - buffer.arrayOffset()];
        System.arraycopy(buffer.array(), buffer.arrayOffset(), raw, 0, buffer.limit());
        return raw;
    }

    public ByteBuffer writeTo(ByteBuffer buffer) throws SVNException {
        if (isAtom()) {
            byte[] data = getData();
            if (useImplicit()) {
                buffer = allocate(buffer, data.length);
                buffer = buffer.put(data);
            } else {
                byte[] sizeBytes = getSizeBytes(data.length);
                if (sizeBytes == null) {
                    SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "Unable to write size bytes to buffer");
                    SVNErrorManager.error(error, SVNLogType.DEFAULT);
                }
                buffer = allocate(buffer, sizeBytes.length + 1 + data.length);
                buffer.put(sizeBytes);

                try {
                    buffer.put(" ".getBytes("UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    buffer.put(" ".getBytes());
                }
                buffer.put(data);
            }
        } else {
            buffer = allocate(buffer, 1);
            try {
                buffer.put("(".getBytes("UTF-8"));
            } catch (UnsupportedEncodingException e) {
                buffer.put("(".getBytes());
            }
            for (Iterator<SVNSkel> iterator = myList.iterator(); iterator.hasNext();) {
                SVNSkel element = (SVNSkel) iterator.next();
                buffer = element.writeTo(buffer);
                if (iterator.hasNext()) {
                    buffer = allocate(buffer, 1);
                    try {
                        buffer.put(" ".getBytes("UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        buffer.put(" ".getBytes());
                    }
                }
            }
            buffer = allocate(buffer, 1);
            try {
                buffer.put(")".getBytes("UTF-8"));
            } catch (UnsupportedEncodingException e) {
                buffer.put(")".getBytes());
            }
        }
        return buffer;
    }

    private int estimateUnparsedSize() {
        if (isAtom()) {
            byte[] data = getData();
            if (data.length < 100) {
// length bytes + whitespace
                return data.length + 3;
            }
            return data.length + 30;
        }
        int total = 2;
        for (Iterator<SVNSkel> iterator = myList.iterator(); iterator.hasNext();) {
            SVNSkel element = (SVNSkel) iterator.next();
            total += element.estimateUnparsedSize();
// space between a pair of elements
            total++;
        }
        return total;
    }

    private boolean useImplicit() {
        byte[] data = getData();
        if (data.length == 0 || data.length >= 100) {
            return false;
        }
        if (getType(data[0]) != TYPE_NAME) {
            return false;
        }
        for (int i = 0; i < data.length; i++) {
            byte cur = data[i];
            if (getType(cur) == TYPE_SPACE || getType(cur) == TYPE_PAREN) {
                return false;
            }
        }
        return true;
    }

    private static ByteBuffer allocate(ByteBuffer buffer, int capacity) {
        if (buffer == null) {
            capacity = Math.max(capacity * 3 / 2, DEFAULT_BUFFER_SIZE);
            return ByteBuffer.allocate(capacity);
        }
        if (capacity > buffer.remaining()) {
            ByteBuffer expandedBuffer = ByteBuffer.allocate((buffer.position() + capacity) * 3 / 2);
            buffer.flip();
            expandedBuffer.put(buffer);
            return expandedBuffer;
        }
        return buffer;
    }

    private static ByteBuffer unread(ByteBuffer buffer, int length) {
        buffer.position(buffer.position() - length);
        return buffer;
    }

    private static int parseSize(ByteBuffer buffer, int limit) {
        limit = limit < 0 ? Integer.MAX_VALUE : limit;
        final int maxPrefix = limit / 10;
        final int maxDigit = limit % 10;
        int value = 0;
        int start = buffer.position();
        while (buffer.hasRemaining()) {
            byte cur = buffer.get();
            if ('0' <= cur && cur <= '9') {
                int digit = cur - '0';
                if (value > maxPrefix || (value == maxPrefix && digit > maxDigit)) {
                    return -1;
                }
                value = (value * 10) + digit;
            } else {
                buffer = unread(buffer, 1);
                break;
            }
        }
        if (start == buffer.position()) {
            return -1;
        }
        return value;
    }

    private static int writeSizeBytes(int value, byte[] data) {
        int i = 0;
        do {
            if (i >= data.length) {
                return -1;
            }
            data[i] = (byte) ((value % 10) + '0');
            value = value / 10;
            i++;
        } while (value > 0);

        for (int left = 0, right = i - 1; left < right; left++, right--) {
            byte tmp = data[left];
            data[left] = data[right];
            data[right] = tmp;
        }
        return i;
    }

    private static byte[] getSizeBytes(final int value) {
        int tmp = value;
        int length = 0;
        do {
            tmp = tmp / 10;
            length++;
        } while (tmp > 0);

        byte[] data = new byte[length];
        int count = writeSizeBytes(value, data);
        if (count < 0) {
            return null;
        }
        if (count < data.length) {
            byte[] result = new byte[count];
            System.arraycopy(data, 0, result, 0, count);
            return result;
        }
        return data;
    }

    private static void error(String type) throws SVNException {
        SVNErrorMessage error = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "Malformed{0}{1} skeleton", new Object[]{type == null ? "" : " ",
                type == null ? "" : type});
        SVNErrorManager.error(error, SVNLogType.DEFAULT);
    }

    public void removeChildren(Collection<SVNSkel> childrenToRemove) {
        for (SVNSkel child : childrenToRemove) {
            myList.remove(child);
        }
        for (int i = 0; i < myList.size(); i++) {
            final SVNSkel skel = myList.get(i);
            skel.myNext = i < myList.size() - 1  ? myList.get(i + 1) : null;
        }
    }

    public void removeAllChildren() {
        myList.clear();
    }
}