/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink

package org.eclipse.persistence.internal.helper;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import static java.lang.Integer.MIN_VALUE;

import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.platform.database.DatabasePlatform;
import org.eclipse.persistence.platform.database.oracle.plsql.PLSQLStoredProcedureCall;
import org.eclipse.persistence.platform.database.oracle.plsql.PLSQLargument;
import org.eclipse.persistence.queries.StoredProcedureCall;
import org.eclipse.persistence.sessions.DatabaseRecord;
import static org.eclipse.persistence.internal.databaseaccess.DatasourceCall.IN;
import static org.eclipse.persistence.internal.databaseaccess.DatasourceCall.OUT;
import static org.eclipse.persistence.internal.helper.Helper.NL;
import static org.eclipse.persistence.internal.helper.Helper.buildHexStringFromBytes;

/**
 * <b>PUBLIC</b>: Interface used to categorize arguments to Stored Procedures as either
 * 'simple' (use subclass SimpleDatabaseType) or 'complex' (use subclass ComplexDatabaseType)
 *
 * @author Mike Norman - michael.norman@oracle.com
 * @since Oracle TopLink 11.x.x
 */
@SuppressWarnings("unchecked")
public interface DatabaseType {

    public static final String TARGET_SHORT_PREFIX = "T_";
    public static final String TARGET_SUFFIX = "TARGET";
    public static final String COMPAT_SHORT_PREFIX = "C_";
    public static final String COMPAT_SUFFIX = "COMPAT";
    public static final int ARGNAME_SIZE_LIMIT = 30 - TARGET_SUFFIX.length();

    public boolean isComplexDatabaseType();

    public boolean isJDBCType();

    public int getSqlCode();

    public int getConversionCode();

    public String getTypeName();

    public int computeInIndex(PLSQLargument inArg, int newIndex,
        ListIterator<PLSQLargument> i);

    public int computeOutIndex(PLSQLargument outArg, int newIndex,
        ListIterator<PLSQLargument> i);

    public void buildInDeclare(StringBuilder sb, PLSQLargument inArg);

    public void buildOutDeclare(StringBuilder sb, PLSQLargument outArg);

    public void buildBeginBlock(StringBuilder sb, PLSQLargument arg, PLSQLStoredProcedureCall call);

    public void buildOutAssignment(StringBuilder sb, PLSQLargument outArg, PLSQLStoredProcedureCall call);

    public void translate(PLSQLargument arg, AbstractRecord translationRow,
        AbstractRecord copyOfTranslationRow, List<DatabaseField> copyOfTranslationFields,
        List<DatabaseField> translationRowFields, List translationRowValues,
        StoredProcedureCall call);

    public void buildOutputRow(PLSQLargument outArg, AbstractRecord outputRow,
        DatabaseRecord newOutputRow, List<DatabaseField> outputRowFields, List outputRowValues);

    public void logParameter(StringBuilder sb, Integer direction, PLSQLargument arg,
        AbstractRecord translationRow, DatabasePlatform platform);

    public enum DatabaseTypeHelper {
        databaseTypeHelper;

        static String getTruncatedSHA1Hash(String s) {
            StringBuilder sb = new StringBuilder(28);
            try {
                byte[] longIdentifierBytes = s.getBytes();
                MessageDigest md = MessageDigest.getInstance("SHA-1");
                md.update(longIdentifierBytes, 0, longIdentifierBytes.length);
                byte[] digest = md.digest(); //produces a 160-bit hash
                //truncate to 112 bits, which is about the same java.util.UUID;
                //HMAC-SHA1-96 is only 96 bits and that's good enough for IPSEC work
                //TL;DR - probability of collision quite small
                byte[] truncDigest = new byte[14];
                System.arraycopy(digest, 0, truncDigest, 0, 14);
                sb.append(buildHexStringFromBytes(truncDigest));
            }
            catch (NoSuchAlgorithmException e) {
                //ignore: should never happen
            }
            return sb.toString();
        }

        protected String getTruncatedSHA1Name(String argName, String prefix) {
            if (argName.length() >= ARGNAME_SIZE_LIMIT) {
                StringBuilder sb = new StringBuilder();
                //the truncated SHA is great, but a PL/SQL identifier
                //can't start with a number, so use prefix
                sb.append(prefix);
                sb.append(getTruncatedSHA1Hash(argName));
                return sb.toString();
            }
            return argName;
        }

        public String buildTarget(PLSQLargument arg) {
            StringBuilder sb = new StringBuilder();
            if (arg.name.length() >= ARGNAME_SIZE_LIMIT) {
                sb.append(getTruncatedSHA1Name(arg.name, TARGET_SHORT_PREFIX));
            }
            else {
                sb.append(arg.name);
                sb.append(TARGET_SUFFIX);
            }
            return sb.toString();
        }

        public String buildCompatible(PLSQLargument arg) {
            StringBuilder sb = new StringBuilder();
            if (arg.name.length() >= ARGNAME_SIZE_LIMIT) {
                sb.append(getTruncatedSHA1Name(arg.name, COMPAT_SHORT_PREFIX));
            }
            else {
                sb.append(arg.name);
                sb.append(COMPAT_SUFFIX);
            }
            return sb.toString();
        }

        public void declareTarget(StringBuilder sb, PLSQLargument arg,
            DatabaseType databaseType) {
            sb.append("  ");
            sb.append(buildTarget(arg));
            sb.append(" ");
            sb.append(databaseType.getTypeName());
        }

        public int computeInIndex(PLSQLargument inArg, int newIndex) {
            inArg.inIndex = newIndex;
            return ++newIndex;
        }

        public int computeOutIndex(PLSQLargument outArg, int newIndex) {
            outArg.outIndex = newIndex;
            return ++newIndex;
        }
        public void buildOutAssignment(StringBuilder sb, PLSQLargument outArg, PLSQLStoredProcedureCall call) {
            sb.append("  :");
            sb.append(outArg.outIndex);
            sb.append(" := ");
            sb.append(buildTarget(outArg));
            sb.append(";");
            sb.append(NL);
        }

        public void translate(PLSQLargument arg, AbstractRecord translationRow,
                AbstractRecord copyOfTranslationRow, List copyOfTranslationFields,
                List translationRowFields, List translationRowValues,
                StoredProcedureCall call) {
            DatabaseField field = null;
            for (Iterator i = copyOfTranslationFields.iterator(); i.hasNext(); ) {
                DatabaseField f = (DatabaseField)i.next();
                if (f.getName().equals(arg.name)) {
                    field = f;
                    break;
                }
            }
            if (arg.length != MIN_VALUE) {
                field.setLength(arg.length);
            }
            if (arg.precision != MIN_VALUE) {
                field.setPrecision(arg.precision);
            }
            if (arg.scale != MIN_VALUE) {
                field.setScale(arg.scale);
            }
            translationRowFields.set(arg.inIndex - 1, field);
            Object value = copyOfTranslationRow.get(field);
            translationRowValues.set(arg.inIndex - 1, value);
        }

        public void buildOutputRow(PLSQLargument outArg, AbstractRecord outputRow,
                DatabaseRecord newOutputRow, List<DatabaseField> outputRowFields, List outputRowValues) {
            DatabaseField field = null;
            for (Iterator i = outputRowFields.iterator(); i.hasNext(); ) {
                DatabaseField f = (DatabaseField)i.next();
                if (f.getName().equals(outArg.name)) {
                    field = f;
                    break;
                }
            }
            Object value = outputRow.get(field);
            newOutputRow.add(field, value);
        }

        public void logParameter(StringBuilder sb, Integer direction, PLSQLargument arg,
            AbstractRecord translationRow, DatabasePlatform platform) {
            if (direction == IN && arg.inIndex != MIN_VALUE) {
                sb.append(":");
                sb.append(arg.inIndex);
                sb.append(" => ");
                sb.append(platform.convertToDatabaseType(translationRow.get(arg.name)));
            }
            if (direction == OUT && arg.outIndex != MIN_VALUE) {
                sb.append(arg.name);
                sb.append(" => :");
                sb.append(arg.outIndex);
            }
        }
    }
}
