/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.openjpa.jdbc.meta;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.identifier.QualifiedDBIdentifier;
import org.apache.openjpa.jdbc.meta.strats.FullClassStrategy;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.schema.Schema;
import org.apache.openjpa.jdbc.schema.SchemaGroup;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.schema.Unique;
import org.apache.openjpa.lib.meta.SourceTracker;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.xml.Commentable;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.MetaDataContext;
import org.apache.openjpa.util.UserException;

/**
 * Information about the mapping from a class to the schema, in raw form.
 * The columns and tables used in mapping info will not be part of the
 * {@link SchemaGroup} used at runtime. Rather, they will be structs
 * with the relevant pieces of information filled in.
 *
 * @author Abe White
 */
public class ClassMappingInfo
    extends MappingInfo
    implements SourceTracker, Commentable {

    private static final long serialVersionUID = 1L;

    private static final Localizer _loc = Localizer.forPackage
        (ClassMappingInfo.class);

    private String _className = Object.class.getName();
    private DBIdentifier _tableName = DBIdentifier.NULL;
    private DBIdentifier _schemaName = DBIdentifier.NULL;
    private boolean _joined = false;
    private Map<DBIdentifier, List<Column>> _seconds = null;
    private String _subStrat = null;
    private File _file = null;
    private int _srcType = SRC_OTHER;
    private String[] _comments = null;
    private int _lineNum = 0;
    private int _colNum = 0;

    // Unique constraints indexed by primary or secondary table name
    private Map<DBIdentifier,List<Unique>> _uniques;

    private Map<DBIdentifier,List<Index>> _indices = new HashMap<>();
    /**
     * The described class name.
     */
    public String getClassName() {
        return _className;
    }

    /**
     * The described class name.
     */
    public void setClassName(String name) {
        _className = name;
    }

    /**
     * The default strategy for subclasses in this hierarchy.
     */
    public String getHierarchyStrategy() {
        return _subStrat;
    }

    /**
     * The default strategy for subclasses in this hierarchy.
     */
    public void setHierarchyStrategy(String strategy) {
        _subStrat = strategy;
    }

    /**
     * The given table name.
     * @deprecated
     */
    @Deprecated
    public String getTableName() {
        return getTableIdentifier().getName();
    }

    public DBIdentifier getTableIdentifier() {
        return _tableName == null ? DBIdentifier.NULL : _tableName;
    }

    /**
     * The given table name.
     * @deprecated
     */
    @Deprecated
    public void setTableName(String table) {
        setTableIdentifier(DBIdentifier.newTable(table));
    }

    public void setTableIdentifier(DBIdentifier table) {
        _tableName = table;
    }

    /**
     * The default schema name for unqualified tables.
     * @deprecated
     */
    @Deprecated
    public String getSchemaName() {
        return getSchemaIdentifier().getName();
    }

    public DBIdentifier getSchemaIdentifier() {
        return _schemaName == null ? DBIdentifier.NULL : _schemaName;
    }

    /**
     * The default schema name for unqualified tables.
     * @deprecated
     */
    @Deprecated
    public void setSchemaName(String schema) {
        setSchemaIdentifier(DBIdentifier.newSchema(schema));
    }

    public void setSchemaIdentifier(DBIdentifier schema) {
        _schemaName = schema;
    }

    /**
     * Whether there is a join to the superclass table.
     */
    public boolean isJoinedSubclass() {
        return _joined;
    }

    /**
     * Whether there is a join to the superclass table.
     */
    public void setJoinedSubclass(boolean joined) {
        _joined = joined;
    }

    /**
     * Return the class-level joined tables.
     * @deprecated
     */
    @Deprecated
    public String[] getSecondaryTableNames() {
        if (_seconds == null)
            return new String[0];
        return DBIdentifier.toStringArray(_seconds.keySet().toArray(new DBIdentifier[]{ }));
    }

    public DBIdentifier[] getSecondaryTableIdentifiers() {
        if (_seconds == null)
            return new DBIdentifier[0];
        return _seconds.keySet().toArray(new DBIdentifier[_seconds.size()]);
    }

    /**
     * We allow fields to reference class-level joins using just the table
     * name, whereas the class join might have schema, etc information.
     * This method returns the name of the given table as listed in a
     * class-level join, or the given name if no join exists.
     * @deprecated
     */
    @Deprecated
    public String getSecondaryTableName(String tableName) {
        return getSecondaryTableIdentifier(DBIdentifier.newTable(tableName)).getName();
    }

    public DBIdentifier getSecondaryTableIdentifier(DBIdentifier tableName) {
        // if no secondary table joins, bad table name, exact match,
        // or an already-qualified table name, nothing to do
        QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(tableName);
        if (_seconds == null || DBIdentifier.isNull(tableName)
            || _seconds.containsKey(tableName)
            || !DBIdentifier.isNull(path.getSchemaName()))
            return tableName;

        // decide which class-level join table is best match
        DBIdentifier best = tableName;
        int pts = 0;
        DBIdentifier fullJoin = DBIdentifier.NULL;
        DBIdentifier join = DBIdentifier.NULL;
        for (DBIdentifier dbIdentifier : _seconds.keySet()) {
            // award a caseless match without schema 2 points
            fullJoin = dbIdentifier;
            QualifiedDBIdentifier joinPath = QualifiedDBIdentifier.getPath(fullJoin);
            if (joinPath.isUnqualifiedObject() && pts < 2 && fullJoin.equalsIgnoreCase(tableName)) {
                best = fullJoin;
                pts = 2;
            }
            else if (joinPath.isUnqualifiedObject())
                continue;

            // immediately return an exact match with schema
            join = joinPath.getIdentifier();
            if (join.equals(tableName))
                return fullJoin;

            // caseless match with schema worth 1 point
            if (pts < 1 && join.equalsIgnoreCase(tableName)) {
                best = fullJoin;
                pts = 1;
            }
        }
        return best;
    }

    /**
     * Return any columns defined for the given class level join, or empty
     * list if none.
     * @deprecated
     */
    @Deprecated
    public List<Column> getSecondaryTableJoinColumns(String tableName) {
        return getSecondaryTableJoinColumns(DBIdentifier.newTable(tableName));
    }

    public List<Column> getSecondaryTableJoinColumns(DBIdentifier tableName) {
        if (_seconds == null || DBIdentifier.isNull(tableName)) {
            return Collections.emptyList();
        }

        // get the columns for the join with the best match for table name
        List<Column> cols = _seconds.get(getSecondaryTableIdentifier(tableName));
        if (cols == null) {
            // possible that given table has extra info the join table
            // doesn't have; strip it
            QualifiedDBIdentifier path = QualifiedDBIdentifier.getPath(tableName);
            if (!DBIdentifier.isNull(path.getSchemaName())) {
                tableName = path.getIdentifier();
                cols = _seconds.get(getSecondaryTableIdentifier(tableName));
            }
        }
        if (cols == null) {
            return Collections.emptyList();
        }
        return cols;
    }

    /**
     * Adds a Secondary table of given name to this mapping. A secondary table
     * must be known before unique constraints are added to a Secondary table.
     * @deprecated
     */
    @Deprecated
    public void addSecondaryTable(String second) {
    	setSecondaryTableJoinColumns(DBIdentifier.newTable(second), null);
    }

    public void addSecondaryTable(DBIdentifier second) {
        setSecondaryTableJoinColumns(second, null);
    }

    /**
     * Declare the given class-level join to the named (secondary) table.
     * @deprecated
     */
    @Deprecated
    public void setSecondaryTableJoinColumns(String tableName, List<Column> cols) {
        if (cols == null)
            cols = Collections.emptyList();
        setSecondaryTableJoinColumns(DBIdentifier.newTable(tableName), cols);
    }

    public void setSecondaryTableJoinColumns(DBIdentifier tableName, List<Column> cols) {
        if (cols == null)
            cols = Collections.emptyList();
        if (_seconds == null)
            _seconds = new LinkedHashMap<>();
        _seconds.put(tableName, cols);
    }

    /**
     * Return the named table for the given class.
     * @deprecated
     */
    @Deprecated
    public Table getTable(final ClassMapping cls, String tableName,
        boolean adapt) {
        return getTable(cls, DBIdentifier.newTable(tableName), adapt);
    }

    /**
     * Return the named table for the given class.
     */
    public Table getTable(final ClassMapping cls, DBIdentifier tableName,
            boolean adapt) {

        Table t = createTable(cls, new TableDefaults() {
            @Override
            public String get(Schema schema) {
                // delay this so that we don't do schema reflection for unique
                // table name unless necessary
                return cls.getMappingRepository().getMappingDefaults().
                    getTableName(cls, schema);
            }
            @Override
            public DBIdentifier getIdentifier(Schema schema) {
                return cls.getMappingRepository().getMappingDefaults().
                    getTableIdentifier(cls, schema);
            }
        }, _schemaName, tableName, adapt);
        t.setComment(cls.getTypeAlias() == null
            ? cls.getDescribedType().getName()
            : cls.getTypeAlias());
        return t;
    }

    /**
     * Return the primary table for the given class.
     */
    public Table getTable(final ClassMapping cls, boolean adapt) {
    	return getTable(cls, _tableName, adapt);
    }

    /**
     * Return the datastore identity columns for the given class, based on the
     * given templates.
     */
    public Column[] getDataStoreIdColumns(ClassMapping cls, Column[] tmplates,
        Table table, boolean adapt) {
        cls.getMappingRepository().getMappingDefaults().
            populateDataStoreIdColumns(cls, table, tmplates);
        return createColumns(cls, "datastoreid", tmplates, table, adapt);
    }

    /**
     * Return the join from this class to its superclass. The table for
     * this class must be set.
     */
    public ForeignKey getSuperclassJoin(final ClassMapping cls, Table table,
        boolean adapt) {
        ClassMapping sup = cls.getJoinablePCSuperclassMapping();
        if (sup == null)
            return null;

        ForeignKeyDefaults def = new ForeignKeyDefaults() {
            @Override
            public ForeignKey get(Table local, Table foreign, boolean inverse) {
                return cls.getMappingRepository().getMappingDefaults().
                    getJoinForeignKey(cls, local, foreign);
            }

            @Override
            public void populate(Table local, Table foreign, Column col,
                Object target, boolean inverse, int pos, int cols) {
                cls.getMappingRepository().getMappingDefaults().
                    populateJoinColumn(cls, local, foreign, col, target,
                        pos, cols);
            }
        };
        return createForeignKey(cls, "superclass", getColumns(), def, table,
            cls, sup, false, adapt);
    }

    /**
     * Synchronize internal information with the mapping data for the given
     * class.
     */
    public void syncWith(ClassMapping cls) {
        clear(false);

        ClassMapping sup = cls.getMappedPCSuperclassMapping();
        if (cls.getTable() != null && (sup == null
            || sup.getTable() != cls.getTable()))
            _tableName = cls.getMappingRepository().getDBDictionary().
                getFullIdentifier(cls.getTable(), true);

        // set io before syncing cols
        setColumnIO(cls.getColumnIO());
        if (cls.getJoinForeignKey() != null && sup != null
            && sup.getTable() != null)
            syncForeignKey(cls, cls.getJoinForeignKey(), cls.getTable(),
                sup.getTable());
        else if (cls.getIdentityType() == ClassMetaData.ID_DATASTORE)
            syncColumns(cls, cls.getPrimaryKeyColumns(), false);

        // record inheritance strategy if class does not use default strategy
        // for base classes, and for all subclasses so we can be sure subsequent
        // mapping runs don't think subclass is unmapped
        String strat = (cls.getStrategy() == null) ? null
            : cls.getStrategy().getAlias();
        if (strat != null && (cls.getPCSuperclass() != null
            || !FullClassStrategy.ALIAS.equals(strat)))
            setStrategy(strat);
    }

    @Override
    public boolean hasSchemaComponents() {
        return super.hasSchemaComponents() || !DBIdentifier.isNull(_tableName);
    }

    @Override
    protected void clear(boolean canFlags) {
        super.clear(canFlags);
        _tableName = DBIdentifier.NULL;
    }

    @Override
    public void copy(MappingInfo info) {
        super.copy(info);
        if (!(info instanceof ClassMappingInfo))
            return;

        ClassMappingInfo cinfo = (ClassMappingInfo) info;
        if (DBIdentifier.isNull(_tableName))
            _tableName = cinfo.getTableIdentifier();
        if (_subStrat == null)
            _subStrat = cinfo.getHierarchyStrategy();
        if (cinfo._seconds != null) {
            if (_seconds == null)
                _seconds = new HashMap<>();
            DBIdentifier key;
            for (DBIdentifier dbIdentifier : cinfo._seconds.keySet()) {
                key = dbIdentifier;
                if (!_seconds.containsKey(key))
                    _seconds.put(key, cinfo._seconds.get(key));
            }
        }
        if (cinfo._uniques != null) {
            if (_uniques == null) {
                _uniques = new HashMap<>();
            }
            for (Entry<DBIdentifier, List<Unique>> entry : cinfo._uniques.entrySet()) {
                if (!_uniques.containsKey(entry.getKey())) {
                    _uniques.put(entry.getKey(), entry.getValue());
                }
            }
        }
        _indices.clear();
        for (Entry<DBIdentifier, List<Index>> entry : cinfo._indices.entrySet()) {
            if (!_indices.containsKey(entry.getKey())) {
                _indices.put(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * Add a unique constraint for the given table.
     * @param table must be primary table or secondary table name added a
     * priori to this receiver.
     * @param unique the unique constraint. null means no-op.
     * @deprecated
     */
    @Deprecated
    public void addUnique(String table, Unique unique) {
        addUnique(DBIdentifier.newTable(table), unique);
    }

    /**
     * Add a unique constraint for the given table.
     * @param table must be primary table or secondary table name added a
     * priori to this receiver.
     * @param unique the unique constraint. null means no-op.
     */
    public void addUnique(DBIdentifier table, Unique unique) {
        if (!DBIdentifier.equal(_tableName, table) &&
            (_seconds == null || !_seconds.containsKey(table))) {
            throw new UserException(_loc.get("unique-no-table",
                    new Object[]{table, _className, _tableName,
                    ((_seconds == null) ? "" : _seconds.keySet())}));
        }
        if (unique == null)
            return;
        if (_uniques == null)
            _uniques = new HashMap<>();
        unique.setTableIdentifier(table);
        List<Unique> uniques = _uniques.get(table);
        if (uniques == null) {
            uniques = new ArrayList<>();
            uniques.add(unique);
            _uniques.put(table, uniques);
        } else {
            uniques.add(unique);
        }
    }

    /**
     * Add index for the given table.
     * @param table must be primary table or secondary table name added a
     * priori to this receiver.
     * @param idx the index. null means no-op.
     */
    public void addIndex(DBIdentifier table, Index idx) {
        if (!DBIdentifier.equal(_tableName, table) &&
           (_seconds == null || !_seconds.containsKey(table))) {
            throw new UserException(_loc.get("index-no-table",
                    new Object[]{table, _className, _tableName,
                    ((_seconds == null) ? "" : _seconds.keySet())}));
        }
        if (idx == null)
            return;
        idx.setTableIdentifier(table);
        List<Index> indices = _indices.get(table);
        if (indices == null) {
            indices = new ArrayList<>();
            indices.add(idx);
            _indices.put(table, indices);
        } else {
            indices.add(idx);
        }
    }

    /**
     * Get the unique constraints of the given primary or secondary table.
     * @deprecated
     */
    @Deprecated
    public Unique[] getUniques(String table) {
        return getUniques(DBIdentifier.newTable(table));
    }

    /**
     * Get the unique constraints of the given primary or secondary table.
     */
    public Unique[] getUniques(DBIdentifier table) {
        if (_uniques == null || _uniques.isEmpty()
        || _uniques.containsKey(table))
            return new Unique[0];
        List<Unique> uniques = _uniques.get(table);
        return uniques.toArray(new Unique[uniques.size()]);
    }

    /**
     * Get all the unique constraints associated with both the primary and/or
     * secondary tables.
     *
     */
    public Unique[] getUniques(MetaDataContext cm, boolean adapt) {
        if (_uniques == null || _uniques.isEmpty())
            return new Unique[0];
        List<Unique> result = new ArrayList<>();
        for (DBIdentifier tableName : _uniques.keySet()) {
            List<Unique> uniqueConstraints = _uniques.get(tableName);
            for (Unique template : uniqueConstraints) {
                Column[] templateColumns = template.getColumns();
                Column[] uniqueColumns = new Column[templateColumns.length];
                Table table = getTable((ClassMapping)cm, tableName, adapt);
                for (int i=0; i<uniqueColumns.length; i++) {
                    DBIdentifier columnName = templateColumns[i].getIdentifier();
                    if (!table.containsColumn(columnName)) {
                        throw new UserException(_loc.get(
                                "unique-missing-column",
                                new Object[]{cm, columnName, tableName,
                                Arrays.toString(table.getColumnNames())}));
                    }
                    Column uniqueColumn = table.getColumn(columnName);
                    uniqueColumns[i] = uniqueColumn;
                }
                Unique unique = createUnique(cm, "unique", template,
                        uniqueColumns, adapt);
                if (unique != null)
                    result.add(unique);
            }
        }
        return result.toArray(new Unique[result.size()]);
    }

    /**
     * Get all indices associated with both the primary and/or
     * secondary tables.
     *
     */
    public Index[] getIndices(MetaDataContext cm, boolean adapt) {
        if (_indices.isEmpty())
            return new Index[0];
        List<Index> result = new ArrayList<>();
        for (DBIdentifier tableName : _indices.keySet()) {
            List<Index> indices = _indices.get(tableName);
            for (Index template : indices) {
                Column[] templateColumns = template.getColumns();
                Column[] columns = new Column[templateColumns.length];
                Table table = getTable((ClassMapping)cm, tableName, adapt);
                for (int i = 0; i < columns.length; i++) {
                    DBIdentifier columnName = templateColumns[i].getIdentifier();
                    if (!table.containsColumn(columnName)) {
                        throw new UserException(_loc.get(
                                "index-missing-column",
                                new Object[]{cm, columnName, tableName,
                                Arrays.toString(table.getColumnNames())}));
                    }
                    Column column = table.getColumn(columnName);
                    columns[i] = column;
                }
                Index idx = createIndex(cm, "index", template, columns, adapt);
                if (idx != null)
                    result.add(idx);
            }
        }
        return result.toArray(new Index[result.size()]);
    }

    @Override
    public File getSourceFile() {
        return _file;
    }

    @Override
    public Object getSourceScope() {
        return null;
    }

    @Override
    public int getSourceType() {
        return _srcType;
    }

    public void setSource(File file, int srcType) {
        _file = file;
        _srcType = srcType;
    }

    @Override
    public String getResourceName() {
        return _className;
    }

    @Override
    public String[] getComments() {
        return (_comments == null) ? EMPTY_COMMENTS : _comments;
    }

    @Override
    public void setComments(String[] comments) {
        _comments = comments;
    }

    @Override
    public int getLineNumber() {
        return _lineNum;
    }

    public void setLineNumber(int lineNum) {
        _lineNum = lineNum;
    }

    @Override
    public int getColNumber() {
        return _colNum;
    }

    public void setColNumber(int colNum) {
        _colNum = colNum;
    }
}
