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

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.*;

import org.tmatesoft.svn.core.ISVNCanceller;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.SVNPropertyValue;
import org.tmatesoft.svn.core.SVNRevisionProperty;
import org.tmatesoft.svn.core.internal.delta.SVNDeltaCombiner;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNHashMap;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNUUIDGenerator;
import org.tmatesoft.svn.core.internal.wc.IOExceptionWrapper;
import org.tmatesoft.svn.core.internal.wc.ISVNCommitPathHandler;
import org.tmatesoft.svn.core.internal.wc.SVNCommitUtil;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.io.diff.SVNDiffWindow;
import org.tmatesoft.svn.util.SVNLogType;


/**
 * @version 1.3
 * @author  TMate Software Ltd.
 */
public class FSRepositoryUtil {
    
    public static final int MAX_KEY_SIZE = 200;
    private static final int BYTES_IN_LONG = Long.SIZE / 8;

    private static final ThreadLocal<byte[]> ourCopyBuffer = new ThreadLocal<byte[]>() {
        @Override
        protected byte[] initialValue() {
            return new byte[1024*16];
        }
    };

    public static String generateLockToken() throws SVNException {
        String uuid = SVNUUIDGenerator.formatUUID(SVNUUIDGenerator.generateUUID());
        return FSFS.SVN_OPAQUE_LOCK_TOKEN + uuid;

    }
    
    public static void replay(FSFS fsfs, FSRoot root, String basePath, long lowRevision, boolean sendDeltas, ISVNEditor editor) throws SVNException {
        Map fsChanges = root.getChangedPaths();
        basePath = basePath.startsWith("/") ? basePath.substring(1) : basePath;
        Collection interestingPaths = new LinkedList();
        Map changedPaths = new SVNHashMap();
        for (Iterator paths = fsChanges.keySet().iterator(); paths.hasNext();) {
            String path = (String) paths.next();
            FSPathChange change = (FSPathChange) fsChanges.get(path);  

            path = path.startsWith("/") ? path.substring(1) : path;
            if (SVNPathUtil.isWithinBasePath(basePath, path)) {
                interestingPaths.add(path);
                changedPaths.put(path, change);
            } else if (SVNPathUtil.isWithinBasePath(path, basePath)) {
                interestingPaths.add(path);
                changedPaths.put(path, change);
            }
        }
        if (FSRepository.isInvalidRevision(lowRevision)) {
            lowRevision = 0;
        }
        
        FSRoot compareRoot = null;
        if (sendDeltas) {
            long revision = -1;
            if (root instanceof FSRevisionRoot) {
                FSRevisionRoot revRoot = (FSRevisionRoot) root;
                revision = revRoot.getRevision() - 1;
            } else if (root instanceof FSTransactionRoot) {
                FSTransactionRoot txnRoot = (FSTransactionRoot) root;
                revision = txnRoot.getTxn().getBaseRevision();
            }
            compareRoot = fsfs.createRevisionRoot(revision);
        }
        
        if (root instanceof FSRevisionRoot) {
            FSRevisionRoot revRoot = (FSRevisionRoot) root;
            editor.targetRevision(revRoot.getRevision());
        }
        
        ISVNCommitPathHandler handler = new FSReplayPathHandler(fsfs, root, compareRoot, changedPaths, basePath, lowRevision);
        SVNCommitUtil.driveCommitEditor(handler, interestingPaths, editor, -1);
    }
    
    public static void copy(InputStream src, OutputStream dst, ISVNCanceller canceller) throws SVNException {
        try {
            byte[] buffer = ourCopyBuffer.get();
            while (true) {
                if (canceller != null) {
                    canceller.checkCancelled();
                }
                int length = src.read(buffer);
                if (length > 0) {
                    dst.write(buffer, 0, length);
                }
                if (length < 0) {
                    break;
                }
            }
        } catch (IOExceptionWrapper ioew) {
            throw ioew.getOriginalException();
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        } 
    }
    
    public static boolean arePropertiesEqual(FSRevisionNode revNode1, FSRevisionNode revNode2) {
        return areRepresentationsEqual(revNode1, revNode2, true);
    }

    public static boolean arePropertiesChanged(FSRoot root1, String path1, FSRoot root2, String path2) throws SVNException {
        FSRevisionNode node1 = root1.getRevisionNode(path1);
        FSRevisionNode node2 = root2.getRevisionNode(path2);
        return !areRepresentationsEqual(node1, node2, true);
    }

    public static boolean areFileContentsChanged(FSRoot root1, String path1, FSRoot root2, String path2) throws SVNException {
        if (root1.checkNodeKind(path1) != SVNNodeKind.FILE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_GENERAL, "''{0}'' is not a file", path1);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        if (root2.checkNodeKind(path2) != SVNNodeKind.FILE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_GENERAL, "''{0}'' is not a file", path2);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        FSRevisionNode revNode1 = root1.getRevisionNode(path1);
        FSRevisionNode revNode2 = root2.getRevisionNode(path2);
        return !areRepresentationsEqual(revNode1, revNode2, false);
    }

    public static SVNProperties getPropsDiffs(SVNProperties sourceProps, SVNProperties targetProps){
        SVNProperties result = new SVNProperties();
        
        if(sourceProps == null){
            sourceProps = new SVNProperties();
        }
        
        if(targetProps == null){
            targetProps = new SVNProperties();
        }
    
        for(Iterator names = sourceProps.nameSet().iterator(); names.hasNext();){
            String propName = (String) names.next();
            SVNPropertyValue srcPropVal = sourceProps.getSVNPropertyValue(propName);
            SVNPropertyValue targetPropVal = targetProps.getSVNPropertyValue(propName);

            if (targetPropVal == null) {
                result.put(propName, targetPropVal);
            } else if (!targetPropVal.equals(srcPropVal)) {
                result.put(propName, targetPropVal);
            }
        }

        for(Iterator names = targetProps.nameSet().iterator(); names.hasNext();){
            String propName = (String)names.next();
            SVNPropertyValue targetPropVal = targetProps.getSVNPropertyValue(propName);
            SVNPropertyValue sourceValue = sourceProps.getSVNPropertyValue(propName);
            if (sourceValue == null){
                result.put(propName, targetPropVal);
            }
        }        
    
        return result;
    }

    public static boolean checkFilesDifferent(FSRoot root1, String path1, FSRoot root2, 
            String path2, SVNDeltaCombiner deltaCombiner) throws SVNException {
        boolean changed = FSRepositoryUtil.areFileContentsChanged(root1, path1, root2, path2);
        if (!changed) {
            return false;
        }

        FSRevisionNode revNode1 = root1.getRevisionNode(path1);
        FSRevisionNode revNode2 = root2.getRevisionNode(path2);
        if (revNode1.getFileLength() != revNode2.getFileLength()) {
            return true;
        }

        if (!revNode1.getFileMD5Checksum().equals(revNode2.getFileMD5Checksum())) {
            return true;
        }

        InputStream file1IS = null;
        InputStream file2IS = null;
        try {
            file1IS = root1.getFileStreamForPath(deltaCombiner, path1);
            file2IS = root2.getFileStreamForPath(deltaCombiner, path2);

            int r1 = -1;
            int r2 = -1;
            while (true) {
                r1 = file1IS.read();
                r2 = file2IS.read();
                if (r1 != r2) {
                    return true;
                }
                if (r1 == -1) {// we've finished - files do not differ
                    break;
                }
            }
        } catch (IOException ioe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, ioe.getLocalizedMessage());
            SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
        } finally {
            SVNFileUtil.closeFile(file1IS);
            SVNFileUtil.closeFile(file2IS);
        }
        return false;
    }

    public static void sendTextDelta(ISVNEditor editor, String editPath, String sourcePath, 
            String hexDigest, FSRevisionRoot sourceRoot, String targetPath, 
            FSRoot targetRoot, boolean sendDeltas, SVNDeltaCombiner deltaCombiner, 
            SVNDeltaGenerator deltaGenerator, FSFS fsfs) throws SVNException {
        editor.applyTextDelta(editPath, hexDigest);

        if (sendDeltas) {
            InputStream sourceStream = null;
            InputStream targetStream = null;
            try {
                if (sourceRoot != null && sourcePath != null) {
                    sourceStream = sourceRoot.getFileStreamForPath(deltaCombiner, sourcePath);
                } else {
                    sourceStream = FSInputStream.createDeltaStream(deltaCombiner, (FSRevisionNode) null, fsfs);
                }
                targetStream = targetRoot.getFileStreamForPath(deltaCombiner, targetPath);
                deltaGenerator.sendDelta(editPath, sourceStream, 0, targetStream, editor, false);
            } finally {
                SVNFileUtil.closeFile(sourceStream);
                SVNFileUtil.closeFile(targetStream);
            }
        } else {
            editor.textDeltaChunk(editPath, SVNDiffWindow.EMPTY);
            editor.textDeltaEnd(editPath);
        }
    }
    
    public static void loadRootChangesOffsetLogicalAddressing(FSFS fsfs, long revision, FSFile file, long index, long[] rootOffset, long[] changesOffset) throws SVNException {
        final long offset = fsfs.lookupOffsetInIndex(file, revision, index);
        if (rootOffset != null) {
            rootOffset[0] = offset;
        }
        if (changesOffset != null) {
            changesOffset[0] = offset;
        }
    }

    public static void loadRootChangesOffset(FSFS fsfs, long revision, FSFile file, long[] rootOffset, long[] changesOffset) throws SVNException {
        ByteBuffer buffer = ByteBuffer.allocate(64);
        long offset = 0; 

        if (fsfs.isPackedRevision(revision) && ((revision + 1) % fsfs.getMaxFilesPerDirectory()) != 0) {
            offset = fsfs.getPackedOffset(revision + 1);
        } else {
            offset = file.size();
        }
        
        long revOffset = 0;
        if (fsfs.isPackedRevision(revision)) {
            revOffset = fsfs.getPackedOffset(revision);
        }

        file.seek(offset - 64);
        try {
            file.read(buffer);
        } catch (IOException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e.getMessage());
            SVNErrorManager.error(err, e, SVNLogType.FSFS);
        }
        
        buffer.flip();
        if (buffer.get(buffer.limit() - 1) != '\n') {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, 
                    "Revision file lacks trailing newline");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        
        int spaceIndex = -1;
        int eolIndex = -1;
        for (int i = buffer.limit() - 2; i >= 0; i--) {
            byte b = buffer.get(i);
            if (b == ' ' && spaceIndex < 0) {
                spaceIndex = i;
            } else if (b == '\n' && eolIndex < 0) {
                eolIndex = i;
                break;
            }
        }
        if (eolIndex < 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, 
                    "Final line in revision file longer than 64 characters");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        if (spaceIndex < 0) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, 
                    "Final line in revision file missing space");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
        try {
            buffer.limit(buffer.limit() - 1);
            buffer.position(spaceIndex + 1);
            String line = decoder.decode(buffer).toString();
            if (changesOffset != null && changesOffset.length > 0) {
                changesOffset[0] = revOffset + Long.parseLong(line);
            }

            buffer.limit(spaceIndex);
            buffer.position(eolIndex + 1);
            line = decoder.decode(buffer).toString();
            if (rootOffset != null && rootOffset.length > 0) {
                rootOffset[0] = revOffset + Long.parseLong(line); 
            }
        } catch (NumberFormatException nfe) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, 
                    "Final line in revision file missing changes and root offsets");
            SVNErrorManager.error(err, nfe, SVNLogType.FSFS);
        } catch (CharacterCodingException e) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_CORRUPT, 
                    "Final line in revision file missing changes and root offsets");
            SVNErrorManager.error(err, e, SVNLogType.FSFS);
        }
    }

    public static String generateNextKey(String oldKey) throws SVNException {
        char[] nextKey = new char[oldKey.length() + 1];
        boolean carry = true;
        if (oldKey.length() > 1 && oldKey.charAt(0) == '0') {
            return null;
        }
        for (int i = oldKey.length() - 1; i >= 0; i--) {
            char c = oldKey.charAt(i);
            if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z'))) {
                return null;
            }
            if (carry) {
                if (c == 'z') {
                    nextKey[i] = '0';
                } else {
                    carry = false;
                    if (c == '9') {
                        nextKey[i] = 'a';
                    } else {
                        nextKey[i] = (char) (c + 1);
                    }
                }
            } else {
                nextKey[i] = c;
            }
        }
        int nextKeyLength = oldKey.length() + (carry ? 1 : 0);
        if (nextKeyLength >= MAX_KEY_SIZE) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, 
                    "FATAL error: new key length is greater than the threshold {0}", 
                    new Object[]{MAX_KEY_SIZE});
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        if (carry) {
            System.arraycopy(nextKey, 0, nextKey, 1, oldKey.length());
            nextKey[0] = '1';
        }
        return new String(nextKey, 0, nextKeyLength);
    }
   
    public static void checkReposDBFormat(int format) throws SVNException {
        if (format == FSFS.MIN_PACKED_REVPROP_SQLITE_DEV_FORMAT) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_UNSUPPORTED_FORMAT, 
                    "Found format '%s', only created by " +
                    "unreleased dev builds; see " +
                    "http://subversion.apache.org" +
                    "/docs/release-notes/1.7#revprop-packing",  
                    new Object[]{FSFS.MIN_PACKED_REVPROP_SQLITE_DEV_FORMAT});
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        if (format < FSFS.DB_FORMAT_LOW || format > FSFS.DB_FORMAT) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_UNSUPPORTED_FORMAT, 
                    "Expected FS format between ''{0}'' and ''{1}''; found format ''{2}''", 
                    new Object[] {FSFS.DB_FORMAT_LOW, FSFS.DB_FORMAT,
                            format});
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
    }
    
    public static void validateProperty(String propertyName, SVNPropertyValue propertyValue) throws SVNException {
        if (!SVNProperty.isRegularProperty(propertyName)) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_BAD_ARGS,
                    "Storage of non-regular property ''{0}'' is disallowed through the repository interface, and could indicate a bug in your client", propertyName);
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }

        if (SVNProperty.isSVNProperty(propertyName) && propertyValue != null) {
            if (SVNRevisionProperty.DATE.equals(propertyName)) {
                try {
                    SVNDate.parseDateString(propertyValue.getString());
                } catch (SVNException svne) {
                    SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_PROPERTY_VALUE);
                    SVNErrorManager.error(err, SVNLogType.FSFS);
                }
            }
        }
    }

    public static long align(long size, long boundary) {
        return (size + boundary - 1) & ~(boundary - 1);
    }

    public static long readLongLittleEndian(RandomAccessFile randomAccessFile) throws IOException {
        final byte[] buffer = new byte[BYTES_IN_LONG];
        final int bytesRead = randomAccessFile.read(buffer, 0, BYTES_IN_LONG);
        if (bytesRead < 0) {
            return bytesRead;
        }
        long value = 0;
        for (int i = BYTES_IN_LONG - 1; i >= 0; i--) {
            value = (value << 8) + (buffer[i] & 0xffL);
        }
        return value;
    }

    public static void writeLongLittleEndian(RandomAccessFile randomAccessFile, long value) throws IOException {
        final byte[] buffer = new byte[BYTES_IN_LONG];
        for (int i = 0; i < BYTES_IN_LONG; i++) {
            buffer[i] = (byte) value;
            value >>= 8;
        }
        randomAccessFile.write(buffer, 0, BYTES_IN_LONG);
    }

    public static int encodeInt(byte[] bytes, long value) {
        return encodeUnsignedInt(bytes, value < 0 ? -1 - 2 * value : 2 * value);
    }

    public static int encodeUnsignedInt(byte[] bytes, long value) {
        //TODO: reuse some code?
        value = value & 0x00000000ffffffffL;

        int i = 0;

        while (value >= 0x80) {
            bytes[i] = (byte) ((value % 0x80) + 0x80);
            value /= 0x80;
            i++;
        }

        bytes[i] = (byte) (value % 0x80);
        return i + 1;
    }

    private static boolean areRepresentationsEqual(FSRevisionNode revNode1, FSRevisionNode revNode2, boolean forProperties) {
        if(revNode1 == revNode2){
            return true;
        }else if(revNode1 == null || revNode2 == null){
            return false;
        }
        return FSRepresentation.compareRepresentations(forProperties ? revNode1.getPropsRepresentation() : revNode1.getTextRepresentation(), forProperties ? revNode2.getPropsRepresentation() : revNode2.getTextRepresentation());
    }
}
