/*
 * ====================================================================
 * Copyright (c) 2004-2010 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.db;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.SqlJetTransactionMode;
import org.tmatesoft.sqljet.core.table.ISqlJetCursor;
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.internal.util.SVNDate;
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.db.Structure;
import org.tmatesoft.svn.core.internal.wc17.db.StructureFields.InheritedProperties;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnChecksum;
import org.tmatesoft.svn.util.SVNLogType;

/**
 * @author TMate Software Ltd.
 */
public abstract class SVNSqlJetStatement {

    protected SVNSqlJetDb sDb;
    private ISqlJetCursor cursor;
    protected List<Object> binds = new ArrayList<Object>();
    protected SqlJetTransactionMode transactionMode = SqlJetTransactionMode.READ_ONLY;

    protected ISqlJetCursor openCursor() throws SVNException {
        throw new UnsupportedOperationException();
    }

    public long insert(Object... data) throws SVNException {
        throw new UnsupportedOperationException();
    }

    public long exec() throws SVNException {
        throw new UnsupportedOperationException();
    }

    public SVNSqlJetStatement(SVNSqlJetDb sDb) {
        this.sDb = sDb;
        setCursor(null);
    }

    public List<Object> getBinds() {
        return binds;
    }

    public boolean isNeedsReset() {
        return getCursor() != null;
    }

    public void reset() throws SVNException {
        binds.clear();
        if (isNeedsReset()) {
            try {
                getCursor().close();
            } catch (SqlJetException e) {
                SVNSqlJetDb.createSqlJetError(e);
            } finally {
                setCursor(null);
                sDb.commit();
            }
        }
    }

    public boolean next() throws SVNException {
        try {
            if (getCursor() == null) {
                sDb.beginTransaction(transactionMode);
                try {
                    setCursor(openCursor());
                    return !getCursor().eof();
                } catch (SVNException e) {
                    if (getCursor() == null) {
                        sDb.commit();
                    }
                    throw e;
                }
            }
            return getCursor().next();
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return false;
        }
    }

    public boolean eof() throws SVNException {
        try {
            if (getCursor() == null) {
                sDb.beginTransaction(transactionMode);
                setCursor(openCursor());
            }
            return getCursor().eof();
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return false;
        }
    }

    public void bindf(String format, Object... data) throws SVNException {

        int n = 0;
        int length = data.length;

        for (int i = 0; i < format.length(); i++) {

            char fmt = format.charAt(i);

            switch (fmt) {
                case 's':
                case 't':
                    if (n > length || data[n] == null) {
                        bindNull(i + 1);
                    } else if (data[n] instanceof File) {
                        bindString(i + 1, SVNFileUtil.getFilePath((File) data[n]));
                    } else {
                        bindString(i + 1, data[n].toString());
                    }
                    n++;
                    break;

                case 'i':
                    if (n > length || data[n] == null) {
                        bindNull(i + 1);
                    } else if (data[n] instanceof Number) {
                        bindLong(i + 1, ((Number) data[n]).longValue());
                    } else if (data[n] instanceof SVNDate) {
                        bindLong(i + 1, ((SVNDate) data[n]).getTimeInMicros());
                    } else {
                        SVNErrorManager.assertionFailure(false, String.format("Number argument required in %d", i + 1), SVNLogType.WC);
                    }
                    n++;
                    break;

                case 'r':
                    if (n > length || data[n] == null) {
                        bindNull(i + 1);
                    } else if (data[n] instanceof Number) {
                        bindRevision(i + 1, ((Number) data[n]).longValue());
                    } else {
                        SVNErrorManager.assertionFailure(false, String.format("Number argument required in %d", i + 1), SVNLogType.WC);
                    }
                    n++;
                    break;

                case 'b':
                    if (n > length || data[n] == null) {
                        bindNull(i + 1);
                    } else if (data[n] instanceof byte[]) {
                        bindBlob(i + 1, (byte[]) data[n]);
                    } else {
                        SVNErrorManager.assertionFailure(false, String.format("Byte array argument required in %d", i + 1), SVNLogType.WC);
                    }
                    n++;
                    break;

                case 'n':
                    bindNull(i + 1);
                    break;

                default:
                    SVNErrorManager.assertionFailure(false, String.format("Unknown format '%s' in %d", fmt, i + 1), SVNLogType.WC);
            }
        }

    }

    private void adjustBinds(int i) {
        int size = binds.size();
        if (size < i) {
            for (int n = size; n < i; n++) {
                binds.add(null);
            }
        }
    }

    public void bindNull(int i) {
        adjustBinds(i);
        binds.set(i - 1, null);
    }

    public void bindLong(int i, long v) {
        adjustBinds(i);
        binds.set(i - 1, v);
    }

    public void bindString(int i, String string) {
        adjustBinds(i);
        binds.set(i - 1, string);
    }

    @SuppressWarnings("unchecked")
    public void bindProperties(int i, SVNProperties props) throws SVNException {
        adjustBinds(i);
        binds.set(i - 1, props != null ? SVNSkel.createPropList(props.asMap()).unparse() : null);
    }

    public void bindIProperties(int i, Map<String, SVNProperties> iprops) throws SVNException {
        adjustBinds(i);
        binds.set(i - 1, iprops != null ? SVNSkel.createInheritedProperties(iprops).unparse() : null);
    }

    public void bindChecksum(int i, SvnChecksum checksum) {
        adjustBinds(i);
        binds.set(i - 1, checksum != null ? checksum.toString() : null);
    }

    public void bindBlob(int i, byte[] serialized) {
        adjustBinds(i);
        binds.set(i - 1, serialized);
    }

    public void bindRevision(int i, long revision) {
        adjustBinds(i);
        if (SVNRevision.isValidRevisionNumber(revision)) {
            bindLong(i, revision);
        } else {
            bindNull(i);
        }
    }

    protected Object getBind(int i) {
        adjustBinds(i);
        return binds.get(i - 1);
    }

    public long count() throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return 0;
            return getCursor().getRowCount();
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return 0;
        }
    }

    public Object getColumn(Enum<?> f) throws SVNException {
        return getColumn(f.toString());
    }

    protected Object getColumn(String f) throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return null;
            return getCursor().getValue(f);
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return null;
        }
    }

    public long getColumnLong(Enum<?> f) throws SVNException {
        return getColumnLong(f.toString());
    }

    protected long getColumnLong(String f) throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return 0;
            return getCursor().getInteger(f);
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return 0;
        }
    }

    public String getColumnString(Enum<?> f) throws SVNException {
        return getColumnString(f.toString());
    }

    protected String getColumnString(String f) throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return null;
            return getCursor().getString(f);
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return null;
        }
    }

    public boolean isColumnNull(Enum<?> f) throws SVNException {
        return isColumnNull(f.toString());
    }

    protected boolean isColumnNull(String f) throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return true;
            return getCursor().isNull(f);
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return false;
        }
    }

    public byte[] getColumnBlob(Enum<?> f) throws SVNException {
        return getColumnBlob(f.toString());
    }

    protected byte[] getColumnBlob(String f) throws SVNException {
        try {
            if (getCursor() == null || getCursor().eof())
                return null;
            return getCursor().getBlobAsArray(f);
        } catch (SqlJetException e) {
            SVNSqlJetDb.createSqlJetError(e);
            return null;
        }
    }

    public boolean getColumnBoolean(Enum<?> f) throws SVNException {
        return getColumnLong(f) != 0;
    }

    public SVNSqlJetStatement getJoinedStatement(String joinedTable) throws SVNException {
        SVNErrorManager.assertionFailure(false, "unsupported", SVNLogType.WC);
        return null;
    }

    public SVNSqlJetStatement getJoinedStatement(Enum<?> joinedTable) throws SVNException {
        return getJoinedStatement(joinedTable.toString());
    }

    public SVNProperties getColumnProperties(Enum<?> f) throws SVNException {
        return getColumnProperties(f.name());
    }
    
    public boolean hasColumnProperties(Enum<?> f) throws SVNException {
        return hasColumnProperties(f.name());
    }

    public List<Structure<InheritedProperties>> getColumnInheritedProperties(Enum<?> f) throws SVNException {
        return getColumnInheritedProperties(f.name());
    }
    
    public boolean hasColumnInheritedProperties(Enum<?> f) throws SVNException {
        return hasColumnInheritedProperties(f.name());
    }

    protected SVNProperties getColumnProperties(String f) throws SVNException {
        if (isColumnNull(f)) {
            return null;
        }
        final byte[] val = getColumnBlob(f);
	    return parseProperties(val);
    }    

    protected boolean hasColumnProperties(String f) throws SVNException {
        if (isColumnNull(f)) {
            return false;
        }
        final byte[] val = getColumnBlob(f);
        return val.length > 2;
    }
    

    public List<Structure<InheritedProperties>> getColumnInheritedProperties(String f) throws SVNException {
        if (isColumnNull(f)) { 
            return null;
        }
        final byte[] val = getColumnBlob(f);
        return parseInheritedProperties(val);
    }

    public boolean hasColumnInheritedProperties(String f) throws SVNException {
        if (isColumnNull(f)) { 
            return false;
        }
        final byte[] val = getColumnBlob(f);
        return val.length > 2;
    }


    public static SVNProperties parseProperties(byte[] val) throws SVNException {
        if (val == null)
            return null;
        final SVNSkel skel = SVNSkel.parse(val);
        if (!skel.isValidPropList()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "proplist");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        return SVNProperties.wrap(skel.parsePropList());
    }

    public static List<Structure<InheritedProperties>> parseInheritedProperties(byte[] val) throws SVNException {
        if (val == null) {
            return null;
        }
        final SVNSkel skel = SVNSkel.parse(val);
        if (!skel.isValidInheritedProperties()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.FS_MALFORMED_SKEL, "inhertied-props");
            SVNErrorManager.error(err, SVNLogType.FSFS);
        }
        return skel.parseInheritedProperties();
    }

    public long done() throws SVNException {
        try {
            return exec();
        } finally {
            reset();
        }
    }

    public void nextRow() throws SVNException {
        if (!next()) {
            SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.SQLITE_ERROR, "Expected database row missing");
            SVNErrorManager.error(err, SVNLogType.DEFAULT);
        }
    }

    public long getColumnRevnum(Enum<?> f) throws SVNException {
        if (isColumnNull(f)) {
            return -1;
        }
        return getColumnLong(f);
    }

    protected ISqlJetCursor getCursor() {
        return cursor;
    }

    protected void setCursor(ISqlJetCursor cursor) {
        this.cursor = cursor;
    }

    public Map<String, Object> getRowValues() throws SVNException {
        throw new UnsupportedOperationException();
    }
}
