package org.tmatesoft.svn.core.internal.wc17.db;

import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.SqlJetTransactionMode;
import org.tmatesoft.sqljet.core.table.ISqlJetCursor;
import org.tmatesoft.sqljet.core.table.ISqlJetTable;
import org.tmatesoft.sqljet.core.table.SqlJetDb;
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.internal.db.SVNSqlJetDb;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetStatement;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetTransaction;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema.NODES__Fields;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema.PRISTINE__Fields;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbStatements;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import static org.tmatesoft.svn.core.internal.wc17.db.SvnWcDbStatementUtil.getColumnChecksum;

public class SvnWcDbPristines extends SvnWcDbShared {
	
	private static final String PRISTINE_STORAGE_EXT = ".svn-base";
	
	 private static class RemoveUnreferencedPristine implements SVNSqlJetTransaction {

	        public SvnChecksum sha1_checksum;
	        public File pristineAbsPath;
	        SVNWCDbRoot root;
	        
	        public void transaction(SVNSqlJetDb db) throws SqlJetException, SVNException {
	        	SVNSqlJetStatement stmt = db.getStatement(SVNWCDbStatements.DELETE_PRISTINE_IF_UNREFERENCED);
                try {
                    stmt.bindf("s", sha1_checksum);
                    Long affectedRows = stmt.done();
                    if (affectedRows > 0) {
                        pristineRemoveFile(true);
                    }
                } finally {
                    stmt.reset();
                }
	        }
	        
	        /* Remove the file at FILE_ABSPATH in such a way that we could re-create a
	         * new file of the same name at any time thereafter.
	         *
	         * On Windows, the file will not disappear immediately from the directory if
	         * it is still being read so the best thing to do is first rename it to a
	         * unique name. */
	        private void pristineRemoveFile(boolean ignoreEnoent) throws SVNException { 
	        	if (SVNFileUtil.isWindows) {
		    		File temDirAbsPath = getPristineTempDir(root, root.getAbsPath());
		    		File tempAbsPath = SVNFileUtil.createUniqueFile(temDirAbsPath, "pristine", ".tmp", false);
		    		try {
		    			SVNFileUtil.rename(pristineAbsPath, tempAbsPath);
		    			pristineAbsPath = tempAbsPath;
		    		}
		    		catch (SVNException e){
		    			if (!(ignoreEnoent && SVNFileType.getType(tempAbsPath) == SVNFileType.NONE))
		    				throw e;
		    		}
	        	}
	    		SVNFileUtil.deleteFile(pristineAbsPath);
	    	}

	    }
	 
	 public static void cleanupPristine(SVNWCDbRoot root, File localAbsPath) throws SVNException {
	    	SVNSqlJetStatement selectList = root.getSDb().getStatement(SVNWCDbStatements.SELECT_UNREFERENCED_PRISTINES);
	        try {
	            while(selectList.next()) {
	                SvnChecksum sha1_checksum = SvnChecksum.fromString(selectList.getColumnString(PRISTINE__Fields.checksum));
	                removePristineIfUnreferenced(root, localAbsPath, sha1_checksum);
	            }
	        } finally {
	        	selectList.reset();
	        }
	    }
	    
	 private static void removePristineIfUnreferenced(SVNWCDbRoot root, File localAbsPath, SvnChecksum sha1_checksum) throws SVNException {
	    	RemoveUnreferencedPristine rup = new RemoveUnreferencedPristine();
	    	rup.sha1_checksum = sha1_checksum;
	    	rup.pristineAbsPath = getPristineFileName(root, sha1_checksum, false);
	    	rup.root = root;
	    	root.getSDb().runTransaction(rup);
	 }
	    
	 public static File getPristineTempDir(SVNWCDbRoot root, File wcRootAbsPath) throws SVNException {
	        return SVNFileUtil.createFilePath(SVNFileUtil.createFilePath(root.getAbsPath(), SVNFileUtil.getAdminDirectoryName()), ISVNWCDb.PRISTINE_TEMPDIR_RELPATH);
	 }
	 
	 public static File getPristineFuturePath(SVNWCDbRoot root, SvnChecksum sha1Checksum) {
		 return getPristineFileName(root, sha1Checksum, false);
	 }
	    
	 public static File getPristineFileName(SVNWCDbRoot root, SvnChecksum sha1Checksum, boolean createSubdir) {
	        /* ### code is in transition. make sure we have the proper data. */
	        assert (root != null);
	        assert (sha1Checksum != null);
	        assert (sha1Checksum.getKind() == SvnChecksum.Kind.sha1);

	        /*
	         * ### need to fix this to use a symbol for ".svn". we don't need ### to
	         * use join_many since we know "/" is the separator for ### internal
	         * canonical paths
	         */
	        File base_dir_abspath = SVNFileUtil.createFilePath(SVNFileUtil.createFilePath(root.getAbsPath(), SVNFileUtil.getAdminDirectoryName()), ISVNWCDb.PRISTINE_STORAGE_RELPATH);

	        String hexdigest = sha1Checksum.getDigest();

	        /* We should have a valid checksum and (thus) a valid digest. */
	        assert (hexdigest != null);

	        /* Get the first two characters of the digest, for the subdir. */
	        String subdir = hexdigest.substring(0, 2);

	        if (createSubdir) {
	            File subdirAbspath = SVNFileUtil.createFilePath(base_dir_abspath, subdir);
	            subdirAbspath.mkdirs();
	            /*
	             * Whatever error may have occurred... ignore it. Typically, this
	             * will be "directory already exists", but if it is something
	             * different*, then presumably another error will follow when we try
	             * to access the file within this (missing?) pristine subdir.
	             */
	        }

	        /* The file is located at DIR/.svn/pristine/XX/XXYYZZ... */
	        return SVNFileUtil.createFilePath(SVNFileUtil.createFilePath(base_dir_abspath, subdir), hexdigest + PRISTINE_STORAGE_EXT);
	 }
	    
	 public static boolean checkPristine(SVNWCDbRoot root, SvnChecksum sha1Checksum) throws SVNException {
	        boolean haveRow = false;
	        SVNSqlJetStatement stmt = root.getSDb().getStatement(SVNWCDbStatements.SELECT_PRISTINE_MD5_CHECKSUM);
	        stmt.bindf("s", sha1Checksum);
	        try {
	            haveRow = stmt.next();
	        } finally {
	            stmt.reset();
	        }
	        if (haveRow) {
	            File pristineAbspath = getPristineFileName(root, sha1Checksum, false);
	            SVNNodeKind kindOnDisk = SVNFileType.getNodeKind(SVNFileType.getType(pristineAbspath));
	            if (kindOnDisk != SVNNodeKind.FILE) {
	                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_DB_ERROR, "The pristine text with checksum ''{0}'' was found in the DB but not on disk", sha1Checksum);
	                SVNErrorManager.error(err, SVNLogType.WC);
	            }
	        }
	        return haveRow;
	 }

	 public static SvnChecksum getPristineSHA1(SVNWCDbRoot root, SvnChecksum md5Checksum) throws SVNException {
	        SVNSqlJetStatement stmt = root.getSDb().getStatement(SVNWCDbStatements.SELECT_PRISTINE_SHA1_CHECKSUM);
	        try {
	            stmt.bindChecksum(1, md5Checksum);
	            boolean have_row = stmt.next();
	            if (!have_row) {
	                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_DB_ERROR, "The pristine text with MD5 checksum ''{0}'' not found", md5Checksum.toString());
	                SVNErrorManager.error(err, SVNLogType.WC);
	                return null;
	            }
	            SvnChecksum sha1_checksum = getColumnChecksum(stmt, PRISTINE__Fields.checksum);
	            assert (sha1_checksum.getKind() == SvnChecksum.Kind.sha1);
	            return sha1_checksum;
	        } finally {
	            stmt.reset();
	        }
	    }
	 
	 public static File getPristinePath(SVNWCDbRoot root, SvnChecksum sha1Checksum) throws SVNException {
	        boolean present = checkPristine(root, sha1Checksum);
	        if (!present) {
	            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_DB_ERROR, "Pristine text not found");
	            SVNErrorManager.error(err, SVNLogType.WC);
	        }
	        return getPristineFileName(root, sha1Checksum, false);
	    }
	 
	 public static void removePristine(SVNWCDbRoot root, SvnChecksum sha1Checksum) throws SVNException {
	        SVNSqlJetStatement stmt = root.getSDb().getStatement(SVNWCDbStatements.LOOK_FOR_WORK);
	        boolean haveRow;
	        try {
	            haveRow = stmt.next();
	        } finally {
	            stmt.reset();
	        }
	        if (haveRow) {
	            return;
	        }
	        pristineRemove(root, sha1Checksum);
	    }

    private static void pristineRemove(SVNWCDbRoot root, SvnChecksum sha1Checksum) throws SVNException {
        SVNSqlJetStatement stmt = root.getSDb().getStatement(SVNWCDbStatements.DELETE_PRISTINE);
        try {
            stmt.bindChecksum(1, sha1Checksum);

            if (stmt.done() != 0) {
                File pristineAbspath = getPristineFileName(root, sha1Checksum, true);
                SVNFileUtil.deleteFile(pristineAbspath);
            }
        } finally {
            stmt.reset();
        }
    }

    public static void installPristine(SVNWCDbRoot root, File tempfileAbspath, SvnChecksum sha1Checksum, SvnChecksum md5Checksum) throws SVNException {
        File pristineAbspath = getPristineFileName(root, sha1Checksum, true);
        if (pristineAbspath.isFile()) {
            SVNFileUtil.deleteFile(tempfileAbspath);
            return;
        }
        SVNFileUtil.rename(tempfileAbspath, pristineAbspath);
        long size = pristineAbspath.length();
        SVNSqlJetStatement stmt = root.getSDb().getStatement(SVNWCDbStatements.INSERT_PRISTINE);
        try {
            stmt.bindChecksum(1, sha1Checksum);
            stmt.bindChecksum(2, md5Checksum);
            stmt.bindLong(3, size);
            stmt.done();
        } finally {
            stmt.reset();
        }
    }
	
	public static InputStream readPristine(SVNWCDbRoot root, File wcRootAbsPath, SvnChecksum sha1Checksum) throws SVNException {
        File pristine_abspath = getPristineFileName(root, sha1Checksum, false);
        return SVNFileUtil.openFileForReading(pristine_abspath);
    }
	
	
	public static void fixPristinesRefCount(SVNWCDbRoot root) throws SVNException {
	    Map<String, Long> refcountTable = new HashMap<String, Long>();
	    begingWriteTransaction(root);
	    try {
	        ISqlJetTable nodesTable = root.getSDb().getDb().getTable(SVNWCDbSchema.NODES.toString());
	        ISqlJetCursor cursor = nodesTable.open();
	        try {
    	        while(!cursor.eof()) {
    	            long opDepth = cursor.getInteger(NODES__Fields.op_depth.toString());
    	            if (opDepth == 0) {
        	            String checksum = cursor.getString(NODES__Fields.checksum.toString());
        	            if (refcountTable.containsKey(checksum)) {
        	                refcountTable.put(checksum, refcountTable.get(checksum) + 1);
        	            } else {
        	                refcountTable.put(checksum, 1l);
        	            }
    	            }
    	            cursor.next();
    	        }
	        } finally {
	            cursor.close();
	        }

	        ISqlJetTable pTable = root.getSDb().getDb().getTable(SVNWCDbSchema.PRISTINE.toString());
	        
	        for (Iterator<String> checksums = refcountTable.keySet().iterator(); checksums.hasNext();) {
	            String checksum = checksums.next();
                cursor = pTable.lookup(null, checksum);
                try {
                    if (!cursor.eof()) {
                        long refCount = cursor.getInteger(PRISTINE__Fields.refcount.toString());
                        if (refCount != refcountTable.get(checksum)) {
                            Map<String, Object> value = new HashMap<String, Object>();
                            cursor.updateByFieldNames(value);
                        }
                        checksums.remove();
                    } 
                } finally {
                    cursor.close();
                }
            }
	    } catch (SqlJetException e) {
	        rollbackTransaction(root);
	        SVNSqlJetDb.createSqlJetError(e);
        } finally {
	        commitTransaction(root);
	    }
	}

    public static void checkPristineChecksumRefcounts(SVNWCDbRoot root) throws SVNException {
        final Map<SvnChecksum, Integer> correctChecksumRefcounts = calculateCorrectChecksumRefcounts(root);
        final Map<SvnChecksum, Integer> checksumRefcountsFromTable = loadChecksumsRefcountsFromTable(root);

        for (Map.Entry<SvnChecksum, Integer> entry : checksumRefcountsFromTable.entrySet()) {
            final SvnChecksum sha1Checksum = entry.getKey();
            final Integer refCountInteger = entry.getValue();
            final Integer correctRefCountInteger = correctChecksumRefcounts.get(sha1Checksum);

            final int refCount = refCountInteger == null ? 0 : refCountInteger;
            final int correctRefCount = correctRefCountInteger == null ? 0 : correctRefCountInteger;

            if (correctRefCount != refCount) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Working copy ''{0}'' is corrupted: {1} table contains incorrect ''refcount'' value {2} for checksum {3} (instead of {4})",
                        root.getAbsPath().getAbsolutePath().replace('/', File.separatorChar), SVNWCDbSchema.PRISTINE.name(), refCount, sha1Checksum, correctRefCount);
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
        }

        for (Map.Entry<SvnChecksum, Integer> entry : correctChecksumRefcounts.entrySet()) {
            final SvnChecksum sha1Checksum = entry.getKey();
            if (!checksumRefcountsFromTable.containsKey(sha1Checksum)) {
                SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_CORRUPT, "Working copy ''{0}'' is corrupted: checksum {1} that is present in {2} table is not listed in {3} table",
                        root.getAbsPath().getAbsolutePath().replace('/', File.separatorChar), sha1Checksum, SVNWCDbSchema.NODES.name(), SVNWCDbSchema.PRISTINE.name());
                SVNErrorManager.error(errorMessage, SVNLogType.WC);
            }
        }
    }

    private static Map<SvnChecksum, Integer> calculateCorrectChecksumRefcounts(SVNWCDbRoot root) throws SVNException {
        Map<SvnChecksum, Integer> checksumToRefCount = new HashMap<SvnChecksum, Integer>();

        final SqlJetDb db = root.getSDb().getDb();
        try {
            final ISqlJetTable nodesTable = db.getTable(SVNWCDbSchema.NODES.name());
            db.beginTransaction(SqlJetTransactionMode.READ_ONLY);
            final ISqlJetCursor cursor = nodesTable.open();

            for (; !cursor.eof(); cursor.next()) {
                String sha1ChecksumString = cursor.getString(SVNWCDbSchema.NODES__Fields.checksum.name());
                if (sha1ChecksumString == null) {
                    continue;
                }
                SvnChecksum sha1Checksum = SvnChecksum.fromString(sha1ChecksumString);

                Integer refCount = checksumToRefCount.get(sha1Checksum);
                int incrementedRefCount = refCount == null ? 1 : refCount + 1;
                checksumToRefCount.put(sha1Checksum, incrementedRefCount);
            }
            cursor.close();
        } catch (SqlJetException e) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_DB_ERROR, e);
            SVNErrorManager.error(errorMessage, e, SVNLogType.WC);
        } finally {
            try {
                db.commit();
            } catch (SqlJetException ignore) {
            }
        }

        return checksumToRefCount;
    }

    private static Map<SvnChecksum, Integer> loadChecksumsRefcountsFromTable(SVNWCDbRoot root) throws SVNException {
        Map<SvnChecksum, Integer> checksumToRefCount = new HashMap<SvnChecksum, Integer>();

        final SqlJetDb db = root.getSDb().getDb();
        try {
            final ISqlJetTable pristineTable = db.getTable(SVNWCDbSchema.PRISTINE.name());
            db.beginTransaction(SqlJetTransactionMode.READ_ONLY);
            final ISqlJetCursor cursor = pristineTable.open();

            for (; !cursor.eof(); cursor.next()) {
                String sha1ChecksumString = cursor.getString(PRISTINE__Fields.checksum.name());
                if (sha1ChecksumString == null) {
                    continue;
                }
                SvnChecksum sha1Checksum = SvnChecksum.fromString(sha1ChecksumString);
                long refcount = cursor.getInteger(PRISTINE__Fields.refcount.name());

                checksumToRefCount.put(sha1Checksum, (int)refcount);
            }
            cursor.close();
        } catch (SqlJetException e) {
            SVNErrorMessage errorMessage = SVNErrorMessage.create(SVNErrorCode.WC_DB_ERROR, e);
            SVNErrorManager.error(errorMessage, e, SVNLogType.WC);
        } finally {
            try {
                db.commit();
            } catch (SqlJetException ignore) {
            }
        }

        return checksumToRefCount;
    }

    public static void transferPristine(SVNWCDb db, File srcLocalAbsPath, File dstWriAbsPath) throws SVNException {
        SVNWCDb.DirParsedInfo srcParsed = db.parseDir(srcLocalAbsPath, SVNSqlJetDb.Mode.ReadOnly);
        SVNWCDb.verifyDirUsable(srcParsed.wcDbDir);
        SVNWCDb.DirParsedInfo dstParsed = db.parseDir(dstWriAbsPath, SVNSqlJetDb.Mode.ReadOnly);
        SVNWCDb.verifyDirUsable(dstParsed.wcDbDir);
        if (srcParsed.wcDbDir.getWCRoot() == dstParsed.wcDbDir.getWCRoot() &&
                srcParsed.wcDbDir.getWCRoot().getSDb() == dstParsed.wcDbDir.getWCRoot().getSDb()) {
            return;
        }

        PristineTransfer pristineTransfer = new PristineTransfer();
        pristineTransfer.srcWcRoot = srcParsed.wcDbDir.getWCRoot();
        pristineTransfer.dstWcRoot = dstParsed.wcDbDir.getWCRoot();
        pristineTransfer.srcRelPath = srcParsed.localRelPath;

        srcParsed.wcDbDir.getWCRoot().getSDb().runTransaction(pristineTransfer);
    }

    private static class PristineTransfer implements SVNSqlJetTransaction {

        public SVNWCDbRoot srcWcRoot;
        public SVNWCDbRoot dstWcRoot;
        public File srcRelPath;

        public void transaction(SVNSqlJetDb db) throws SqlJetException, SVNException {
            SVNSqlJetStatement stmt = db.getStatement(SVNWCDbStatements.SELECT_COPY_PRISTINES);
            try {
                stmt.bindf("is", srcWcRoot.getWcId(), srcRelPath);
                boolean gotRow = stmt.next();

                while (gotRow) {
                    SvnChecksum checksum = SvnWcDbStatementUtil.getColumnChecksum(stmt, NODES__Fields.checksum);
                    SvnChecksum md5Checksum = SvnWcDbStatementUtil.getColumnChecksum(stmt.getJoinedStatement(SVNWCDbSchema.PRISTINE), PRISTINE__Fields.md5_checksum);
                    long size = stmt.getJoinedStatement(SVNWCDbSchema.PRISTINE).getColumnLong(PRISTINE__Fields.size);

                    maybeTransferOnePristine(srcWcRoot, dstWcRoot, checksum, md5Checksum, size);

                    gotRow = stmt.next();
                }
            } finally {
                stmt.reset();
            }
        }
    }

    private static void maybeTransferOnePristine(SVNWCDbRoot srcWcRoot, SVNWCDbRoot dstWcRoot, SvnChecksum checksum, SvnChecksum md5Checksum, long size) throws SVNException {
        SVNSqlJetStatement stmt = dstWcRoot.getSDb().getStatement(SVNWCDbStatements.INSERT_OR_IGNORE_PRISTINE);
        try {
            stmt.bindChecksum(1, checksum);
            stmt.bindChecksum(2, md5Checksum);
            stmt.bindLong(3, size);
            long affectedRows = stmt.done();
            if (affectedRows == 0) {
                return;
            }
        } finally {
            stmt.reset();
        }

        File srcAbsPath = getPristineFileName(srcWcRoot, checksum, false);
        File dstAbsPath = getPristineFileName(dstWcRoot, checksum, true);

        SVNFileUtil.copy(srcAbsPath, dstAbsPath, true, false);
    }
}

