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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeSet;

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.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class SVNWCProperties {
    public static final String SVN_HASH_TERMINATOR = "END";
    
    private File myFile;

    private String myPath;

    public SVNWCProperties(File properitesFile, String path) {
        myFile = properitesFile;
        myPath = path;
    }

    public File getFile() {
        return myFile;
    }

    public String getPath() {
        return myPath;
    }

    public Collection properties(Collection target) throws SVNException {
        target = target == null ? new TreeSet() : target;
        if (isEmpty()) {
            return target;
        }
        ByteArrayOutputStream nameOS = new ByteArrayOutputStream();
        InputStream is = SVNFileUtil.openFileForReading(getFile(), SVNLogType.WC);
        try {
            while (readProperty('K', is, nameOS)) {
                target.add(new String(nameOS.toByteArray(), "UTF-8"));
                nameOS.reset();
                readProperty('V', is, null);
            }
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
            SVNErrorManager.error(err, e, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(is);
        }
        return target;
    }

    public SVNProperties asMap() throws SVNException {
        SVNProperties result = new SVNProperties();
        if (isEmpty()) {
            return result;
        }
        ByteArrayOutputStream nameOS = new ByteArrayOutputStream();
        InputStream is = SVNFileUtil.openFileForReading(getFile(), SVNLogType.WC);
        try {
            while (readProperty('K', is, nameOS)) {
                String name = new String(nameOS.toByteArray(), "UTF-8");
                nameOS.reset();
                readProperty('V', is, nameOS);
                byte[] value = nameOS.toByteArray();
                result.put(name, value);
                nameOS.reset();
            }
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, "Cannot read properties file ''{0}'': {1}", new Object[] {getFile(), e.getLocalizedMessage()});
            SVNErrorManager.error(err, e, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(is);
        }
        return result;
    }

    public boolean compareTo(SVNWCProperties properties,
            ISVNPropertyComparator comparator) throws SVNException {
        boolean equals = true;
        Collection props1 = properties(null);
        Collection props2 = properties.properties(null);

        // missed in props2.
        Collection tmp = new TreeSet(props1);
        tmp.removeAll(props2);
        for (Iterator props = tmp.iterator(); props.hasNext();) {
            String missing = (String) props.next();
            comparator.propertyDeleted(missing);
            equals = false;
        }

        // added in props2.
        tmp = new TreeSet(props2);
        tmp.removeAll(props1);

        File tmpFile = null;
        File tmpFile1 = null;
        File tmpFile2 = null;
        OutputStream os = null;
        InputStream is = null;
        InputStream is1 = null;
        InputStream is2 = null;

        for (Iterator props = tmp.iterator(); props.hasNext();) {
            String added = (String) props.next();
            try {
                tmpFile = SVNFileUtil.createUniqueFile(getFile().getParentFile(), getFile().getName(), ".tmp", true);

                os = SVNFileUtil.openFileForWriting(tmpFile);
                properties.getPropertyValue(added, os);
                SVNFileUtil.closeFile(os);

                is = SVNFileUtil.openFileForReading(tmpFile, SVNLogType.WC);
                comparator.propertyAdded(added, is, (int) tmpFile.length());
                equals = false;
                SVNFileUtil.closeFile(is);
            } finally {
                if (tmpFile != null) {
                    tmpFile.delete();
                }
                SVNFileUtil.closeFile(os);
                SVNFileUtil.closeFile(is);
                tmpFile = null;
                is = null;
                os = null;
            }
        }

        // changed in props2
        props2.retainAll(props1);
        for (Iterator props = props2.iterator(); props.hasNext();) {
            String changed = (String) props.next();

            try {
                tmpFile1 = SVNFileUtil.createUniqueFile(getFile().getParentFile(), getFile().getName(), ".tmp1", true);
                tmpFile2 = SVNFileUtil.createUniqueFile(getFile().getParentFile(), getFile().getName(), ".tmp2", true);

                os = SVNFileUtil.openFileForWriting(tmpFile1);
                getPropertyValue(changed, os);
                os.close();
                os = SVNFileUtil.openFileForWriting(tmpFile2);
                properties.getPropertyValue(changed, os);
                os.close();
                if (tmpFile2.length() != tmpFile1.length()) {
                    is = SVNFileUtil.openFileForReading(tmpFile2, SVNLogType.WC);
                    comparator.propertyChanged(changed, is, (int) tmpFile2
                            .length());
                    equals = false;
                    SVNFileUtil.closeFile(is);
                } else {
                    is1 = SVNFileUtil.openFileForReading(tmpFile1, SVNLogType.WC);
                    is2 = SVNFileUtil.openFileForReading(tmpFile2, SVNLogType.WC);
                    boolean differs = false;
                    for (int i = 0; i < tmpFile1.length(); i++) {
                        if (is1.read() != is2.read()) {
                            differs = true;
                            break;
                        }
                    }
                    SVNFileUtil.closeFile(is1);
                    SVNFileUtil.closeFile(is2);
                    if (differs) {
                        is2 = SVNFileUtil.openFileForReading(tmpFile2, SVNLogType.WC);
                        comparator.propertyChanged(changed, is2, (int) tmpFile2
                                .length());
                        equals = false;
                        SVNFileUtil.closeFile(is2);
                    }
                }
            } catch (IOException e) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
                SVNErrorManager.error(err, e, SVNLogType.WC);
            } finally {
                if (tmpFile2 != null) {
                    tmpFile2.delete();
                }
                if (tmpFile1 != null) {
                    tmpFile1.delete();
                }
                SVNFileUtil.closeFile(os);
                SVNFileUtil.closeFile(is);
                SVNFileUtil.closeFile(is1);
                SVNFileUtil.closeFile(is2);
                os = null;
                tmpFile1 = tmpFile2 = null;
                is = is1 = is2 = null;
            }
        }
        return equals;
    }

    public String getPropertyValue(String name) throws SVNException {
        if (isEmpty()) {
            return null;
        }
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        os = (ByteArrayOutputStream) getPropertyValue(name, os);
        if (os != null && os.size() >= 0) {
            byte[] bytes = os.toByteArray();
            try {
                return new String(bytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                return new String(bytes);
            }
        }
        return null;
    }

    public OutputStream getPropertyValue(String name, OutputStream os) throws SVNException {
        if (isEmpty()) {
            return null;
        }
        ByteArrayOutputStream nameOS = new ByteArrayOutputStream();
        InputStream is = SVNFileUtil.openFileForReading(getFile(), SVNLogType.WC);
        try {
            while (readProperty('K', is, nameOS)) {
                String currentName = new String(nameOS.toByteArray(), "UTF-8");
                nameOS.reset();
                if (currentName.equals(name)) {
                    readProperty('V', is, os);
                    return os;
                }
                readProperty('V', is, null);                
            }
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
            SVNErrorManager.error(err, e, SVNLogType.WC);
        } finally {
            SVNFileUtil.closeFile(is);
        }
        return null;
    }

    public void setPropertyValue(String name, SVNPropertyValue value) throws SVNException {
        byte[] bytes = SVNPropertyValue.getPropertyAsBytes(value);
        int length = bytes != null && bytes.length >= 0 ? bytes.length : -1;
        setPropertyValue(name, bytes != null ? new ByteArrayInputStream(bytes) : null, length);
    }

    public void setPropertyValue(String name, InputStream is, int length)
            throws SVNException {
        InputStream src = null;
        OutputStream dst = null;
        File tmpFile = null;
        boolean empty = false;
        try {
            tmpFile = SVNFileUtil.createUniqueFile(getFile().getParentFile(), getFile().getName(), ".tmp", true);
            if (!isEmpty()) {
                src = SVNFileUtil.openFileForReading(getFile(), SVNLogType.WC);
            }
            dst = SVNFileUtil.openFileForWriting(tmpFile);
            empty = !copyProperties(src, dst, name, is, length);
        } finally {
            SVNFileUtil.closeFile(src);
            SVNFileUtil.closeFile(dst);
        }
        if (tmpFile != null) {
            if (!empty) {
                SVNFileUtil.rename(tmpFile, getFile());
                SVNFileUtil.setReadonly(getFile(), true);
            } else {
                SVNFileUtil.deleteFile(tmpFile);
                SVNFileUtil.deleteFile(getFile());
            }
        }
    }

    public void setProperties(SVNProperties properties) throws SVNException {
        if (properties != null) {
            for (Iterator names = properties.nameSet().iterator(); names.hasNext();) {
                String name = (String) names.next();
                SVNPropertyValue value = properties.getSVNPropertyValue(name);
                setPropertyValue(name, value);
            }
        }
    }
    
    public SVNProperties compareTo(SVNWCProperties properties) throws SVNException {
        final SVNProperties locallyChangedProperties = new SVNProperties();
        compareTo(properties, new ISVNPropertyComparator() {
            public void propertyAdded(String name, InputStream value, int length) {
                propertyChanged(name, value, length);
            }

            public void propertyChanged(String name, InputStream newValue,
                    int length) {
                ByteArrayOutputStream os = new ByteArrayOutputStream(length);
                for (int i = 0; i < length; i++) {
                    try {
                        os.write(newValue.read());
                    } catch (IOException e) {
                    }
                }
                byte[] bytes = os.toByteArray();
                try {
                    locallyChangedProperties.put(name, new String(bytes, "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    locallyChangedProperties.put(name, new String(bytes));
                }
            }

            public void propertyDeleted(String name) {
                locallyChangedProperties.put(name, (SVNPropertyValue) null);
            }
        });
        return locallyChangedProperties;
    }

    public static void setProperties(SVNProperties namesToValues, File target, File tmpFile, String terminator) throws SVNException {
        OutputStream dst = null;
        try {
            if (tmpFile != null) {
                tmpFile.getParentFile().mkdirs();
            } else {
                target.getParentFile().mkdirs();
            }
            dst = SVNFileUtil.openFileForWriting(tmpFile != null ? tmpFile : target);
            setProperties(namesToValues, dst, terminator);
        } finally {
            SVNFileUtil.closeFile(dst);
        }
        if (tmpFile != null && target != null) {
            target.getParentFile().mkdirs();
            SVNFileUtil.rename(tmpFile, target);
        } 
        if (target != null) {
            SVNFileUtil.setReadonly(target, true);
        }
    }

    public static void setProperties(SVNProperties namesToValues, OutputStream target, String terminator) throws SVNException {
        try {
            Object[] keys = namesToValues.nameSet().toArray();
            Arrays.sort(keys);
            for (int i = 0; i < keys.length; i++) {
                String propertyName = (String) keys[i];
                writeProperty(target, 'K', propertyName.getBytes("UTF-8"));
                writeProperty(target, 'V', SVNPropertyValue.getPropertyAsBytes(namesToValues.getSVNPropertyValue(propertyName)));
            }
            if (terminator != null) {
                target.write(terminator.getBytes("UTF-8"));
                target.write('\n');
            }
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.WC);
        }
    }
    
    public static void appendProperty(String name, SVNPropertyValue value, OutputStream target) throws SVNException {
        if (name == null || value == null){
            return;
        }

        byte[] bytes = SVNPropertyValue.getPropertyAsBytes(value);        

        try {
            writeProperty(target, 'K', name.getBytes("UTF-8"));
            writeProperty(target, 'V', bytes);
        }catch(IOException ioe){    
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.WC);
        }
    }
    
    public static void appendPropertyDeleted(String name, OutputStream target) throws SVNException {
        if(name == null){
            return;
        }
        try {
            writeProperty(target, 'D', name.getBytes("UTF-8"));
        }catch(IOException ioe){    
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.WC);
        }
    }
    
    /** @noinspection ResultOfMethodCallIgnored */
    private static boolean copyProperties(InputStream is, OutputStream os,
            String name, InputStream value, int length) throws SVNException {
        // read names, till name is met, then insert value or skip this
        // property.
        int propCount = 0;
        try {
            if (is != null) {
                int l = 0;
                while ((l = readLength(is, 'K')) > 0) {
                    byte[] nameBytes = new byte[l];
                
                    SVNFileUtil.readIntoBuffer(is, nameBytes, 0, nameBytes.length);
                    is.read();
                    if (name.equals(new String(nameBytes, "UTF-8"))) {
                        // skip property, will be appended.
                        readProperty('V', is, null);
                        continue;
                    }
                    // save name
                    writeProperty(os, 'K', nameBytes);
                    l = readLength(is, 'V');
                    writeProperty(os, 'V', is, l);
                    is.read();
                    propCount++;
                }
            }
            if (value != null && length >= 0) {
                byte[] nameBytes = name.getBytes("UTF-8");
                writeProperty(os, 'K', nameBytes);
                writeProperty(os, 'V', value, length);
                propCount++;
            }
            if (propCount > 0) {
                os.write(new byte[] { 'E', 'N', 'D', '\n' });
            }
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getLocalizedMessage());
            SVNErrorManager.error(err, e, SVNLogType.WC);
        }
        return propCount > 0;
    }

    private static boolean readProperty(char type, InputStream is, OutputStream os) throws IOException {
        int length = readLength(is, type);
        if (length < 0) {
            return false;
        }
        if (os != null) {
            byte[] value = new byte[length];
            
            int r = SVNFileUtil.readIntoBuffer(is, value, 0, value.length);
            
            if (r >= 0) {
                os.write(value, 0, r);
            } else {
                return false;
            }
        } else {
            while(length > 0) {
                length -= is.skip(length);
            }
        }
        return is.read() == '\n';
    }

    private static void writeProperty(OutputStream os, char type, byte[] value)
            throws IOException {
        os.write((byte) type);
        os.write(' ');
        os.write(Integer.toString(value.length).getBytes("UTF-8"));
        os.write('\n');
        os.write(value);
        os.write('\n');
    }

    private static void writeProperty(OutputStream os, char type,
            InputStream value, int length) throws IOException {
        os.write((byte) type);
        os.write(' ');
        os.write(Integer.toString(length).getBytes("UTF-8"));
        os.write('\n');
        for (int i = 0; i < length; i++) {
            int r = value.read();
            os.write(r);
        }
        os.write('\n');
    }

    private static int readLength(InputStream is, char type) throws IOException {
        byte[] buffer = new byte[255];
        int r = SVNFileUtil.readIntoBuffer(is, buffer, 0, 4);
        if (r != 4) {
            throw new IOException("invalid properties file format");
        }
        // either END\n or K x\n
        if (buffer[0] == 'E' && buffer[1] == 'N' && buffer[2] == 'D' && buffer[3] == '\n') {
            return -1;
        } else if (buffer[0] == type && buffer[1] == ' ') {
            int i = 4;
            if (buffer[3] != '\n') {
                while (true) {
                    int b = is.read();
                    if (b < 0) {
                        throw new IOException("invalid properties file format");
                    } else if (b == '\n') {
                        break;
                    }
                    buffer[i] = (byte) (0xFF & b);
                    i++;
                }
            } else {
                i = 3;
            }
            String length = new String(buffer, 2, i - 2, "UTF-8");
            return Integer.parseInt(length.trim());
        }
        throw new IOException("invalid properties file format");
    }

    public boolean isEmpty() {
        return !getFile().exists() || getFile().length() <= 4;
    }
}
