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

import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.schema.SqlJetConflictAction;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetDb;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetDeleteStatement;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetInsertStatement;
import org.tmatesoft.svn.core.internal.db.SVNSqlJetSelectStatement;
import org.tmatesoft.svn.core.internal.util.SVNDate;
import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
import org.tmatesoft.svn.core.internal.util.SVNSkel;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext;
import org.tmatesoft.svn.core.internal.wc17.SVNWCContext.CheckWCRootInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbKind;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.SVNWCDbStatus;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbBaseInfo;
import org.tmatesoft.svn.core.internal.wc17.db.ISVNWCDb.WCDbBaseInfo.BaseInfoField;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.DirParsedInfo;
import org.tmatesoft.svn.core.internal.wc17.db.SVNWCDb.InsertBase;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.ExternalNodeInfo;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema;
import org.tmatesoft.svn.core.internal.wc17.db.statement.SVNWCDbSchema.EXTERNALS__Fields;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.util.SVNLogType;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

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

public class SvnWcDbExternals extends SvnWcDbShared {
    
    public static void addExternalDir(SVNWCDb db, File localAbspath, File wriPath, SVNURL reposRootUrl, String reposUuid,
            File recordAncestorAbspath, File recordedReposRelPath, long recordedPegRevision, long recordedRevision,
            SVNSkel workItems) throws SVNException {

        if (wriPath == null) {
            wriPath = SVNFileUtil.getParentFile(localAbspath);
        }
        DirParsedInfo dirInfo = db.obtainWcRoot(wriPath);
        SVNWCDbRoot root = dirInfo.wcDbDir.getWCRoot();
        
        File localRelpath = SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), localAbspath.getAbsolutePath()));
        Structure<ExternalNodeInfo> externalInfo = Structure.obtain(ExternalNodeInfo.class);
        
        externalInfo.set(ExternalNodeInfo.revision, SVNWCContext.INVALID_REVNUM);
        externalInfo.set(ExternalNodeInfo.changedRevision, SVNWCContext.INVALID_REVNUM);
        externalInfo.set(ExternalNodeInfo.reposId, SVNWCContext.INVALID_REVNUM);
        externalInfo.set(ExternalNodeInfo.recordedRevision, SVNWCContext.INVALID_REVNUM);
        externalInfo.set(ExternalNodeInfo.recordedPegRevision, SVNWCContext.INVALID_REVNUM);
        
        externalInfo.set(ExternalNodeInfo.kind, SVNWCDbKind.Dir);
        externalInfo.set(ExternalNodeInfo.presence, SVNWCDbStatus.Normal);
        externalInfo.set(ExternalNodeInfo.reposRootUrl, reposRootUrl);
        externalInfo.set(ExternalNodeInfo.reposUuid, reposUuid);
        externalInfo.set(ExternalNodeInfo.recordAncestorRelPath, SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), recordAncestorAbspath.getAbsolutePath())));
        externalInfo.set(ExternalNodeInfo.recordedReposRelPath, recordedReposRelPath);
        
        externalInfo.set(ExternalNodeInfo.recordedPegRevision, recordedPegRevision);
        externalInfo.set(ExternalNodeInfo.recordedRevision, recordedRevision);
        
        externalInfo.set(ExternalNodeInfo.workItems, workItems);
        
        begingWriteTransaction(root);
        try {
            insertExternalNode(root, localRelpath, externalInfo);
        } catch (SVNException e) {
            rollbackTransaction(root);
            throw e;
        } finally {
            commitTransaction(root);
            externalInfo.release();
        }

    }
    
    public static void insertExternalNode(SVNWCDbRoot root, File localRelpath, Structure<ExternalNodeInfo> info) throws SVNException {
        long reposId = info.lng(ExternalNodeInfo.reposId);
        if (reposId == SVNWCContext.INVALID_REVNUM) {
            reposId = root.getDb().createReposId(root.getSDb(), info.<SVNURL>get(ExternalNodeInfo.reposRootUrl), info.text(ExternalNodeInfo.reposUuid));
        }
        boolean updateRoot;
        SVNWCDbStatus status;
        try {
            WCDbBaseInfo baseInfo = root.getDb().getBaseInfo(root, localRelpath, BaseInfoField.updateRoot, BaseInfoField.status);
            updateRoot = baseInfo.updateRoot;
            status = baseInfo.status;
            if (status == SVNWCDbStatus.Normal && !updateRoot) {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_UNEXPECTED_STATUS);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        } catch (SVNException e) {
            if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                throw e;
            }
        }
        
        SVNWCDbKind kind = info.<SVNWCDbKind>get(ExternalNodeInfo.kind);
        if (kind == SVNWCDbKind.File || kind == SVNWCDbKind.Symlink) {
            InsertBase insertBase = root.getDb().new InsertBase();
            insertBase.localRelpath = localRelpath;
            insertBase.status = SVNWCDbStatus.Normal;
            insertBase.kind = info.<SVNWCDbKind>get(ExternalNodeInfo.kind);
            
            insertBase.reposId = reposId;
            insertBase.reposRelpath = info.<File>get(ExternalNodeInfo.reposRelPath);
            insertBase.revision = info.lng(ExternalNodeInfo.revision);
            
            insertBase.props = info.<SVNProperties>get(ExternalNodeInfo.properties);
            insertBase.changedRev = info.lng(ExternalNodeInfo.changedRevision);
            insertBase.changedDate = info.<SVNDate>get(ExternalNodeInfo.changedDate);
            insertBase.changedAuthor = info.text(ExternalNodeInfo.changedAuthor);
            
            insertBase.davCache = info.<SVNProperties>get(ExternalNodeInfo.davCache);
            insertBase.checksum = info.<SvnChecksum>get(ExternalNodeInfo.checksum);

            insertBase.target = info.<File>get(ExternalNodeInfo.target);
            insertBase.conflict = info.<SVNSkel>get(ExternalNodeInfo.conflict);
            
            insertBase.updateActualProps = info.is(ExternalNodeInfo.updateActualProperties);
            insertBase.actualProps = info.<SVNProperties>get(ExternalNodeInfo.newActualProperties);
            insertBase.keepRecordedInfo = info.is(ExternalNodeInfo.keepRecordedInfo);
            insertBase.workItems = info.<SVNSkel>get(ExternalNodeInfo.workItems);
            
            insertBase.fileExternal = true;
            insertBase.iprops = info.<Map<String, SVNProperties>>get(ExternalNodeInfo.iprops);
            insertBase.wcId = root.getWcId();
            insertBase.wcRoot = root;
            
            try {
                insertBase.transaction(root.getSDb());
            } catch (SqlJetException e) {
                sqliteError(e);
            }
        } else {
            root.getDb().addWorkQueue(root.getAbsPath(), info.<SVNSkel>get(ExternalNodeInfo.workItems));
        }
        //
        SVNSqlJetInsertStatement stmt = new InsertExternalStatement(root.getSDb());
        try {
            stmt.bindf("issttsisii",
                    root.getWcId(),
                    localRelpath,
                    SVNFileUtil.getFileDir(localRelpath),
                    getPresenceText(info.<SVNWCDbStatus>get(ExternalNodeInfo.presence)),
                    getKindText(info.<SVNWCDbKind>get(ExternalNodeInfo.kind)),
                    info.get(ExternalNodeInfo.recordAncestorRelPath),
                    reposId,
                    info.get(ExternalNodeInfo.recordedReposRelPath),
                    info.lng(ExternalNodeInfo.recordedPegRevision),
                    info.lng(ExternalNodeInfo.recordedRevision));
            stmt.done();
        } finally {
            stmt.reset();
        }
    }

    public static void removeExternal(SVNWCContext context, File wriAbsPath, File localAbsPath) throws SVNException {
        Structure<ExternalNodeInfo> info = readExternal(context, localAbsPath, wriAbsPath, ExternalNodeInfo.presence, ExternalNodeInfo.kind);
        removeExternalNode(context, localAbsPath, wriAbsPath, null);
        SVNWCDbKind kind = info.<SVNWCDbKind>get(ExternalNodeInfo.kind);
        info.release();
        if (kind == SVNWCDbKind.Dir) {
            CheckWCRootInfo wcRootInfo = context.checkWCRoot(localAbsPath, false);
            if (wcRootInfo != null && wcRootInfo.wcRoot) {
                context.removeFromRevisionControl(localAbsPath, true, false);
            }
        } else {
            try {
                WCDbBaseInfo baseInfo = context.getDb().getBaseInfo(localAbsPath, BaseInfoField.updateRoot);
                if (baseInfo != null && !baseInfo.updateRoot) {
                    return;
                }                
            } catch (SVNException e) {
                if (e.getErrorMessage().getErrorCode() != SVNErrorCode.WC_PATH_NOT_FOUND) {
                    throw e;
                }
            }
            context.getDb().removeBase(localAbsPath);
            SVNFileUtil.deleteFile(localAbsPath);
        }
    }
    
    public static void removeExternalNode(SVNWCContext context, File localAbsPath, File wriAbsPath, SVNSkel workItems) throws SVNException {
        if (wriAbsPath == null) {
            wriAbsPath = SVNFileUtil.getParentFile(localAbsPath);
        }
        DirParsedInfo dirInfo = ((SVNWCDb) context.getDb()).obtainWcRoot(wriAbsPath);
        SVNWCDbRoot root = dirInfo.wcDbDir.getWCRoot();
        
        File localRelpath = SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), localAbsPath.getAbsolutePath()));
        begingWriteTransaction(root);
        SVNSqlJetDeleteStatement deleteExternal = null;
        try {
            deleteExternal = new SVNSqlJetDeleteStatement(root.getSDb(), SVNWCDbSchema.EXTERNALS);
            try {
                deleteExternal.bindf("is", root.getWcId(), localRelpath);
                deleteExternal.done();
            } finally {
                deleteExternal.reset();
            }

            if (workItems != null) {
                root.getDb().addWorkQueue(root.getAbsPath(), workItems);
            }
        } catch (SVNException e) {
            rollbackTransaction(root);
        } finally {
            commitTransaction(root);
        }
        
    }
    
    public static Structure<ExternalNodeInfo> readExternal(SVNWCContext context, File localAbsPath, File wriAbsPath, ExternalNodeInfo... fields) throws SVNException {
        if (wriAbsPath == null) {
            wriAbsPath = SVNFileUtil.getParentFile(localAbsPath);
        }
        DirParsedInfo dirInfo = ((SVNWCDb) context.getDb()).obtainWcRoot(wriAbsPath);
        SVNWCDbRoot root = dirInfo.wcDbDir.getWCRoot();
        
        File localRelpath = SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), localAbsPath.getAbsolutePath()));
        SVNSqlJetSelectStatement selectExternalInfo = new SVNSqlJetSelectStatement(root.getSDb(), SVNWCDbSchema.EXTERNALS);
        try {
            selectExternalInfo.bindf("is", root.getWcId(), localRelpath);
            if (selectExternalInfo.next()) {
                Structure<ExternalNodeInfo> info = Structure.obtain(ExternalNodeInfo.class, fields);
                if (info.hasField(ExternalNodeInfo.presence)) {
                    info.set(ExternalNodeInfo.presence, getColumnPresence(selectExternalInfo, EXTERNALS__Fields.presence));
                }
                if (info.hasField(ExternalNodeInfo.kind)) {
                    info.set(ExternalNodeInfo.kind, getColumnKind(selectExternalInfo, EXTERNALS__Fields.kind));
                }
                if (info.hasField(ExternalNodeInfo.definingAbsPath)) {
                    info.set(ExternalNodeInfo.definingAbsPath, SVNFileUtil.createFilePath(root.getAbsPath(), selectExternalInfo.getColumnString(EXTERNALS__Fields.def_local_relpath)));
                }
                // TODO read more                
                return info;
            } else {
                SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.WC_PATH_NOT_FOUND, "The node ''{0}'' is not an external.", localAbsPath);
                SVNErrorManager.error(err, SVNLogType.WC);
            }
        } finally {
            reset(selectExternalInfo);
        }
        return null;
    }
    
    /**
     * INSERT OR REPLACE INTO externals (
     * wc_id, local_relpath, parent_relpath, presence, kind, def_local_relpath,
     * repos_id, def_repos_relpath, def_operational_revision, def_revision)
     * VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
     */
    private static class InsertExternalStatement extends SVNSqlJetInsertStatement {

        public InsertExternalStatement(SVNSqlJetDb sDb) throws SVNException {
            super(sDb, SVNWCDbSchema.EXTERNALS, SqlJetConflictAction.REPLACE);
        }
        
        @Override
        protected Map<String, Object> getInsertValues() throws SVNException {
            Map<String, Object> values = new HashMap<String, Object>();
            values.put(EXTERNALS__Fields.wc_id.toString(), getBind(1));
            values.put(EXTERNALS__Fields.local_relpath.toString(), getBind(2));
            values.put(EXTERNALS__Fields.parent_relpath.toString(), getBind(3));
            values.put(EXTERNALS__Fields.presence.toString(), getBind(4));
            values.put(EXTERNALS__Fields.kind.toString(), getBind(5));
            values.put(EXTERNALS__Fields.def_local_relpath.toString(), getBind(6));
            values.put(EXTERNALS__Fields.repos_id.toString(), getBind(7));
            values.put(EXTERNALS__Fields.def_repos_relpath.toString(), getBind(8));
            if (((Long) getBind(9)) >= 0) {
                values.put(EXTERNALS__Fields.def_operational_revision.toString(), getBind(9));
            } else {
                values.put(EXTERNALS__Fields.def_operational_revision.toString(), null);
            }
            if (((Long) getBind(10)) >= 0) {
                values.put(EXTERNALS__Fields.def_revision.toString(), getBind(10));
            } else {
                values.put(EXTERNALS__Fields.def_revision.toString(), null);
            }
            return values;
        }
        
    }

    public static void addExternalFile(SVNWCContext context, File localAbsPath, File wriAbsPath, File reposRelPath, 
            SVNURL reposRootUrl, String reposUuid, 
            long targetRevision, SVNProperties newPristineProperties, Map<String, SVNProperties> iprops, long changedRev,
            SVNDate changedDate, String changedAuthor, SvnChecksum newChecksum, SVNProperties davCache, 
            File recordAncestorAbspath, File recordedReposRelPath, long recordedPegRevision, long recordedRevision, 
            boolean updateActualProperties, SVNProperties newActualProperties, boolean keepRecordedInfo, 
            SVNSkel conflictSkel, SVNSkel allWorkItems) throws SVNException {
        SVNWCDb db = (SVNWCDb) context.getDb();
        if (wriAbsPath == null) {
            wriAbsPath = SVNFileUtil.getParentFile(localAbsPath);
        }
        DirParsedInfo dirInfo = db.obtainWcRoot(wriAbsPath);
        SVNWCDbRoot root = dirInfo.wcDbDir.getWCRoot();
        
        File localRelpath = SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), localAbsPath.getAbsolutePath()));
        Structure<ExternalNodeInfo> externalInfo = Structure.obtain(ExternalNodeInfo.class);

        externalInfo.set(ExternalNodeInfo.kind, SVNWCDbKind.File);
        externalInfo.set(ExternalNodeInfo.presence, SVNWCDbStatus.Normal);
        
        externalInfo.set(ExternalNodeInfo.reposRootUrl, reposRootUrl);
        externalInfo.set(ExternalNodeInfo.reposUuid, reposUuid);
        externalInfo.set(ExternalNodeInfo.reposId, SVNWCContext.INVALID_REVNUM);
        
        externalInfo.set(ExternalNodeInfo.reposRelPath, reposRelPath);
        externalInfo.set(ExternalNodeInfo.revision, targetRevision);
        externalInfo.set(ExternalNodeInfo.properties, newPristineProperties);
        externalInfo.set(ExternalNodeInfo.iprops, iprops); 

        externalInfo.set(ExternalNodeInfo.changedRevision, changedRev);
        externalInfo.set(ExternalNodeInfo.changedDate, changedDate);
        externalInfo.set(ExternalNodeInfo.changedAuthor, changedAuthor);
        externalInfo.set(ExternalNodeInfo.checksum, newChecksum);
        externalInfo.set(ExternalNodeInfo.davCache, davCache);

        externalInfo.set(ExternalNodeInfo.recordAncestorRelPath, SVNFileUtil.createFilePath(SVNPathUtil.getRelativePath(root.getAbsPath().getAbsolutePath(), recordAncestorAbspath.getAbsolutePath())));
        externalInfo.set(ExternalNodeInfo.recordedRevision, recordedRevision);
        externalInfo.set(ExternalNodeInfo.recordedPegRevision, recordedPegRevision);
        externalInfo.set(ExternalNodeInfo.recordedReposRelPath, recordedReposRelPath);
        
        externalInfo.set(ExternalNodeInfo.updateActualProperties, updateActualProperties);
        externalInfo.set(ExternalNodeInfo.newActualProperties, newActualProperties);
        externalInfo.set(ExternalNodeInfo.keepRecordedInfo, keepRecordedInfo);

        externalInfo.set(ExternalNodeInfo.conflict, conflictSkel);
        externalInfo.set(ExternalNodeInfo.workItems, allWorkItems);
        
        begingWriteTransaction(root);
        try {
            insertExternalNode(root, localRelpath, externalInfo);
        } catch (SVNException e) {
            rollbackTransaction(root);
            throw e;
        } finally {
            commitTransaction(root);
            externalInfo.release();
        } 
    }

}
