/*
 * 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
//     tware - added handling of database delimiters
//     11/19/2012-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support (foreign key metadata support)
//     11/22/2012-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support (index metadata support)
//     12/07/2012-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support (foreign key metadata support)
package org.eclipse.persistence.internal.helper;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.persistence.internal.core.helper.CoreTable;
import org.eclipse.persistence.internal.databaseaccess.*;
import org.eclipse.persistence.internal.expressions.ExpressionSQLPrinter;
import org.eclipse.persistence.tools.schemaframework.ForeignKeyConstraint;
import org.eclipse.persistence.tools.schemaframework.IndexDefinition;

/**
 * INTERNAL:
 * <p> <b>Purpose</b>:
 * Define a fully qualified table name.<p>
 * <b>Responsibilities</b>:    <ul>
 *    <li> Allow specification of a qualifier to the table, i.e. creator or database.
 *    </ul>
 *@see DatabaseField
 */
public class DatabaseTable implements CoreTable, Cloneable, Serializable {
    protected String name;
    protected String tableQualifier;
    protected String qualifiedName;

    /** JPA 2.1 Foreign key specification data */
    protected Map<String, ForeignKeyConstraint> foreignKeyConstraints;

    /**
     * Contains the user specified unique constraints. JPA 2.0 introduced
     * the name element, therefore, if specified we will use that name
     * to create the constraint. Constraints with no name will be added to the
     * map under the null key and generated with a default name.
     * Therefore, when a name is given the list size should only ever be
     * 1. We will validate. The null key could have multiples however they will
     * have their names defaulted (as we did before).
     */
    protected Map<String, List<List<String>>> uniqueConstraints;

    /**
     * Store the set of indexes defined through meta-data for the table.
     */
    protected List<IndexDefinition> indexes;

    protected boolean useDelimiters = false;

    protected String creationSuffix;

    /**
     * Initialize the newly allocated instance of this class.
     * By default their is no qualifier.
     */
    public DatabaseTable() {
        name = "";
        tableQualifier = "";
    }

    public DatabaseTable(String possiblyQualifiedName) {
        this(possiblyQualifiedName, null, null);
    }

    public DatabaseTable(String possiblyQualifiedName, String startDelimiter, String endDelimiter) {
        setPossiblyQualifiedName(possiblyQualifiedName, startDelimiter, endDelimiter);
    }

    public DatabaseTable(String tableName, String qualifier) {
        this(tableName, qualifier, false, null, null);
    }

    public DatabaseTable(String tableName, String qualifier, boolean useDelimiters, String startDelimiter, String endDelimiter) {
        setName(tableName, startDelimiter, endDelimiter);
        this.tableQualifier = qualifier;
        this.useDelimiters = useDelimiters;
    }

    public void addForeignKeyConstraint(ForeignKeyConstraint foreignKeyConstraint) {
        if (foreignKeyConstraints == null) {
            foreignKeyConstraints = new HashMap<String, ForeignKeyConstraint>();
        }

        foreignKeyConstraints.put(foreignKeyConstraint.getName(), foreignKeyConstraint);
    }

    /**
     * Add an index definition to this table.
     */
    public void addIndex(IndexDefinition index) {
        getIndexes().add(index);
    }

    /**
     * Add the unique constraint for the columns names. Used for DDL generation.
     * For now we just add all the unique constraints as we would have before
     * when we didn't have a name.
     */
    public void addUniqueConstraints(String name, List<String> columnNames) {
        if (getUniqueConstraints().containsKey(name)) {
            getUniqueConstraints().get(name).add(columnNames);
        } else {
            List<List<String>> value = new ArrayList<List<String>>();
            value.add(columnNames);
            getUniqueConstraints().put(name, value);
        }
    }

    /**
     * Return a shallow copy of the receiver.
     */
    public DatabaseTable clone() {
        try {
            return (DatabaseTable)super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new InternalError(exception.getMessage());
        }
    }

    /**
     * Two tables are equal if their names and tables are equal,
     * or their names are equal and one does not have a qualifier assigned.
     * This allows an unqualified table to equal the same fully qualified one.
     */
    public boolean equals(Object object) {
        if (object instanceof DatabaseTable) {
            return equals((DatabaseTable)object);
        }
        return false;
    }

    /**
     * Two tables are equal if their names and tables are equal,
     * or their names are equal and one does not have a qualifier assigned.
     * This allows an unqualified table to equal the same fully qualified one.
     */
    public boolean equals(DatabaseTable table) {
        if (this == table) {
            return true;
        }
        if (table == null) {
            return false;
        }
        if (DatabasePlatform.shouldIgnoreCaseOnFieldComparisons) {
            if (this.name.equalsIgnoreCase(table.name)) {
                if ((this.tableQualifier.length() == 0) || (table.tableQualifier.length() == 0) || (this.tableQualifier.equalsIgnoreCase(table.tableQualifier))) {
                    return true;
                }
            }
        } else {
            if (this.name.equals(table.name)) {
                if ((this.tableQualifier.length() == 0) || (table.tableQualifier.length() == 0) || (this.tableQualifier.equals(table.tableQualifier))) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * returns the suffix applied to the CREATE table statement on this field for DDL generation.
     */
    public String getCreationSuffix() {
        return creationSuffix;
    }

    public ForeignKeyConstraint getForeignKeyConstraint(String name) {
        return foreignKeyConstraints.get(name);
    }

    public Map<String, ForeignKeyConstraint> getForeignKeyConstraints() {
        return foreignKeyConstraints;
    }

    /**
     * Return a list of index definitions.
     * Used for DDL generation.
     */
    public List<IndexDefinition> getIndexes() {
        if (this.indexes == null) {
            this.indexes = new ArrayList<IndexDefinition>();
        }
        return this.indexes;
    }

    /**
     * Get method for table name.
     */
    public String getName() {
        return name;
    }

    /**
     * Get method for table name.
     */
    public String getNameDelimited(DatasourcePlatform platform) {
        if (useDelimiters){
            return platform.getStartDelimiter() + name + platform.getEndDelimiter();
        }
        return name;
    }

    public String getQualifiedName() {
        if (qualifiedName == null) {
            if (tableQualifier.equals("")) {
                qualifiedName = getName();
            } else {
                qualifiedName = getTableQualifier() + "." + getName();
            }
        }

        return qualifiedName;
    }

    public String getQualifiedNameDelimited(DatasourcePlatform platform) {
        if (tableQualifier.equals("")) {
            if (useDelimiters){
                return platform.getStartDelimiter() + getName() + platform.getEndDelimiter();
            } else {
                return getName();
            }
        } else {
            if (useDelimiters){
                return platform.getStartDelimiter() + getTableQualifier() + platform.getEndDelimiter() + "."
                  + platform.getStartDelimiter() + getName() + platform.getEndDelimiter();
            } else {
                return getTableQualifier() + "." + getName();
            }
        }
    }

    /**
     * Print the table's SQL from clause.
     */
    public void printSQL(ExpressionSQLPrinter printer) throws IOException {
        printer.getWriter().write(getQualifiedNameDelimited(printer.getPlatform()));
    }

    public String getTableQualifierDelimited(DatasourcePlatform platform) {
        if (useDelimiters && tableQualifier != null && !tableQualifier.equals("")){
            return platform.getStartDelimiter() + tableQualifier + platform.getEndDelimiter();
        }
        return tableQualifier;
    }

    public String getTableQualifier() {
        return tableQualifier;
    }

    public boolean hasUniqueConstraints() {
        return (this.uniqueConstraints != null) && (!this.uniqueConstraints.isEmpty());
    }

    public boolean hasForeignKeyConstraints() {
        return foreignKeyConstraints != null;
    }

    /**
     * Return the hashcode of the name, because it is fairly unique.
     */
    public int hashCode() {
        return getName().hashCode();
    }

    public boolean hasIndexes() {
        return (this.indexes != null) && (!this.indexes.isEmpty());
    }

    /**
     * Return a list of the unique constraints for this table.
     * Used for DDL generation.
     */
    public Map<String, List<List<String>>> getUniqueConstraints() {
        if (this.uniqueConstraints == null) {
            this.uniqueConstraints = new HashMap<String, List<List<String>>>();
        }
        return this.uniqueConstraints;
    }

    /**
     * Determine whether the receiver has any identification information.
     * Return true if the name or qualifier of the receiver are nonempty.
     */
    public boolean hasName() {
        if ((getName().length() == 0) && (getTableQualifier().length() == 0)) {
            return false;
        }

        return true;
    }

    /**
     * INTERNAL:
     * Is this decorated / has an AS OF (some past time) clause.
     * <b>Example:</b>
     * SELECT ... FROM EMPLOYEE AS OF TIMESTAMP (exp) t0 ...
     */
    public boolean isDecorated() {
        return false;
    }

    protected void resetQualifiedName() {
        this.qualifiedName = null;
    }

    public void setCreationSuffix(String creationSuffix) {
        this.creationSuffix = creationSuffix;
    }

    /**
     * Set the table name.
     * Used when aliasing table names.
     * @param name
     */
    public void setName(String name) {
        setName(name, null, null);
    }

    /**
     * Set the table name.
     * Used when aliasing table names.
     *
     * If the name contains database delimiters, they will be stripped and a flag will be set to have them
     * added when the DatabaseTable is written to SQL
     *
     * @param name
     */
    public void setName(String name, String startDelimiter, String endDelimiter) {
        if (name != null && (startDelimiter != null) && (endDelimiter != null) && !startDelimiter.equals("")&& !endDelimiter.equals("") && name.startsWith(startDelimiter) && name.endsWith(endDelimiter)){
            this.name = name.substring(startDelimiter.length(), name.length() - endDelimiter.length());
            useDelimiters = true;
        } else {
            this.name = name ;
        }
        resetQualifiedName();
    }

    /**
     * Used to map the project xml. Any time a string name is read from the
     * project xml, we must check if it is fully qualified and split the
     * actual name from the qualifier.
     *
     * @param possiblyQualifiedName
     */
    public void setPossiblyQualifiedName(String possiblyQualifiedName) {
        setPossiblyQualifiedName(possiblyQualifiedName, null, null);
    }

    public void setPossiblyQualifiedName(String possiblyQualifiedName, String startDelimiter, String endDelimiter) {
        resetQualifiedName();

        int index = possiblyQualifiedName.lastIndexOf('.');

        if (index == -1) {
            setName(possiblyQualifiedName, startDelimiter, endDelimiter);
            this.tableQualifier = "";
        } else {
            setName(possiblyQualifiedName.substring(index + 1, possiblyQualifiedName.length()), startDelimiter, endDelimiter);
            setTableQualifier(possiblyQualifiedName.substring(0, index), startDelimiter, endDelimiter);

            if((startDelimiter != null) && possiblyQualifiedName.startsWith(startDelimiter) && (endDelimiter != null) && possiblyQualifiedName.endsWith(endDelimiter)) {
                // It's 'Qualifier.Name' - it should be treated as a single string.
                // Would that be 'Qualifier'.'Name' both setName and setTableQualifier methods would have set useDelimeters to true.
                if(!this.useDelimiters) {
                    setName(possiblyQualifiedName);
                    this.tableQualifier = "";
                }
            }
        }
    }

    public void setTableQualifier(String qualifier) {
        setTableQualifier(qualifier, null, null);
    }

    public void setTableQualifier(String qualifier, String startDelimiter, String endDelimiter) {
        if ((startDelimiter != null) && (endDelimiter != null) && !startDelimiter.equals("")&& !endDelimiter.equals("") && qualifier.startsWith(startDelimiter) && qualifier.endsWith(endDelimiter)){
            this.tableQualifier = qualifier.substring(startDelimiter.length(), qualifier.length() - endDelimiter.length());
            useDelimiters = true;
        } else {
            this.tableQualifier = qualifier;
        }
        resetQualifiedName();
    }

    public String toString() {
        return "DatabaseTable(" + getQualifiedName() + ")";
    }

    public void setUseDelimiters(boolean useDelimiters) {
        this.useDelimiters = useDelimiters;
    }

    public boolean shouldUseDelimiters() {
        return useDelimiters;
    }
}
