/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.seasar.extension.jdbc.dialect;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;

import javax.persistence.GenerationType;
import javax.persistence.TemporalType;

import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.SelectForUpdateType;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.types.BytesType;
import org.seasar.extension.jdbc.types.SerializableType;
import org.seasar.extension.jdbc.types.ValueTypes;
import org.seasar.extension.jdbc.types.BytesType.Trait;
import org.seasar.framework.util.tiger.Pair;

/**
 * PostgreSQL用の方言をあつかうクラスです。
 * 
 * @author higa
 * 
 */
public class PostgreDialect extends StandardDialect {

    /**
     * 一意制約違反を表すSQLステート
     */
    protected static final String uniqueConstraintViolationCode = "23505";

    /**
     * BLOB用の値タイプです。
     */
    protected final static ValueType BLOB_TYPE = new BytesType(
            new PostgreTrait());

    /**
     * オブジェクトをシリアライズしたBLOB用の値タイプです。
     */
    public final static ValueType SERIALIZABLE_BLOB_TYPE = new SerializableType(
            new PostgreTrait());

    @Override
    public String getName() {
        return "postgre";
    }

    @Override
    public boolean supportsLimit() {
        return true;
    }

    @Override
    public boolean needsParameterForResultSet() {
        return true;
    }

    @Override
    public String convertLimitSql(String sql, int offset, int limit) {
        StringBuilder buf = new StringBuilder(sql.length() + 20);
        buf.append(sql);
        if (limit > 0) {
            buf.append(" limit ");
            buf.append(limit);
        }
        if (offset > 0) {
            buf.append(" offset ");
            buf.append(offset);
        }
        return buf.toString();
    }

    @Override
    public ValueType getValueType(PropertyMeta propertyMeta) {
        final Class<?> clazz = propertyMeta.getPropertyClass();
        if (propertyMeta.isLob()) {
            if (clazz == String.class) {
                return ValueTypes.STRING;
            } else if (clazz == byte[].class) {
                return BLOB_TYPE;
            } else if (Serializable.class.isAssignableFrom(clazz)) {
                return SERIALIZABLE_BLOB_TYPE;
            }
        }
        final ValueType valueType = getValueTypeInternal(clazz);
        if (valueType != null) {
            return valueType;
        }
        return super.getValueType(propertyMeta);
    }

    @Override
    public ValueType getValueType(Class<?> clazz, boolean lob,
            TemporalType temporalType) {
        if (lob) {
            if (clazz == String.class) {
                return ValueTypes.STRING;
            } else if (clazz == byte[].class) {
                return BLOB_TYPE;
            } else if (Serializable.class.isAssignableFrom(clazz)) {
                return SERIALIZABLE_BLOB_TYPE;
            }
        }
        return super.getValueType(clazz, lob, temporalType);
    }

    @Override
    protected ValueType getValueTypeInternal(Class<?> clazz) {
        if (List.class.isAssignableFrom(clazz)) {
            return ValueTypes.POSTGRE_RESULT_SET;
        }
        return null;
    }

    @Override
    public GenerationType getDefaultGenerationType() {
        return GenerationType.IDENTITY;
    }

    @Override
    public boolean supportsIdentity() {
        return true;
    }

    @Override
    public boolean supportsGetGeneratedKeys() {
        return false;
    }

    @Override
    public String getIdentitySelectString(final String tableName,
            final String columnName) {
        return new String(new StringBuilder(64).append("select currval('")
                .append(tableName).append('_').append(columnName).append(
                        "_seq')"));
    }

    @Override
    public boolean supportsSequence() {
        return true;
    }

    @Override
    public String getSequenceNextValString(final String sequenceName,
            final int allocationSize) {
        return "select nextval('" + sequenceName + "')";
    }

    @Override
    public boolean supportsForUpdate(final SelectForUpdateType type,
            boolean withTarget) {
        return type == SelectForUpdateType.NORMAL;
    }

    @Override
    public String getForUpdateString(final SelectForUpdateType type,
            final int waitSeconds, final Pair<String, String>... aliases) {
        final StringBuilder buf = new StringBuilder(100).append(" for update");
        if (aliases.length > 0) {
            buf.append(" of ");
            for (final Pair<String, String> alias : aliases) {
                buf.append(alias.getFirst()).append(", ");
            }
            buf.setLength(buf.length() - 2);
        }
        return new String(buf);
    }

    @Override
    public boolean supportsOuterJoinForUpdate() {
        return false;
    }

    @Override
    public boolean isUniqueConstraintViolation(Throwable t) {
        final String state = getSQLState(t);
        return uniqueConstraintViolationCode.equals(state);
    }

    /**
     * {@link Blob}を扱うトレイトです。
     * 
     * @author koichik
     */
    public static class PostgreTrait implements Trait {

        public int getSqlType() {
            return Types.BLOB;
        }

        public void set(final PreparedStatement ps, final int parameterIndex,
                final byte[] bytes) throws SQLException {
            ps.setBlob(parameterIndex, new BlobImpl(bytes));
        }

        public void set(final CallableStatement cs, final String parameterName,
                final byte[] bytes) throws SQLException {
            cs.setBytes(parameterName, bytes);
        }

        public byte[] get(final ResultSet rs, final int columnIndex)
                throws SQLException {
            return BytesType.toBytes(rs.getBlob(columnIndex));
        }

        public byte[] get(final ResultSet rs, final String columnName)
                throws SQLException {
            return BytesType.toBytes(rs.getBlob(columnName));
        }

        public byte[] get(final CallableStatement cs, final int columnIndex)
                throws SQLException {
            return BytesType.toBytes(cs.getBlob(columnIndex));
        }

        public byte[] get(final CallableStatement cs, final String columnName)
                throws SQLException {
            return BytesType.toBytes(cs.getBlob(columnName));
        }

    }

    /**
     * {@link Blob}の簡易実装クラスです。
     * 
     * @author koichik
     */
    public static class BlobImpl implements Blob {

        /** バイト列 */
        protected byte[] bytes;

        /**
         * インスタンスを構築します。
         * 
         * @param bytes
         *            バイト列
         */
        public BlobImpl(final byte[] bytes) {
            this.bytes = bytes;
        }

        public InputStream getBinaryStream() throws SQLException {
            return new ByteArrayInputStream(bytes);
        }

        public byte[] getBytes(final long pos, final int length)
                throws SQLException {
            if (length == bytes.length) {
                return bytes;
            }
            final byte[] result = new byte[length];
            System.arraycopy(bytes, 0, result, 0, length);
            return result;
        }

        public long length() throws SQLException {
            return bytes.length;
        }

        public long position(final Blob pattern, final long start)
                throws SQLException {
            throw new UnsupportedOperationException("position");
        }

        public long position(final byte[] pattern, final long start)
                throws SQLException {
            throw new UnsupportedOperationException("position");
        }

        public OutputStream setBinaryStream(final long pos) throws SQLException {
            throw new UnsupportedOperationException("setBinaryStream");
        }

        public int setBytes(final long pos, final byte[] bytes,
                final int offset, final int len) throws SQLException {
            throw new UnsupportedOperationException("setBytes");
        }

        public int setBytes(final long pos, final byte[] bytes)
                throws SQLException {
            throw new UnsupportedOperationException("setBytes");
        }

        public void truncate(final long len) throws SQLException {
            throw new UnsupportedOperationException("truncate");
        }

    }

}
