/*
 * 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.schema;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.lib.meta.XMLMetaDataSerializer;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.xml.sax.SAXException;

/**
 * Serializes {@link Schema}s to XML matching the document
 * type definition defined by the {@link XMLSchemaParser}. The serializer
 * actually works at the fine-grained table level to allow you to split
 * schemas among multiple files.
 *  Serializers are not thread safe.
 *
 * @author Abe White
 */
public class XMLSchemaSerializer
    extends XMLMetaDataSerializer
    implements SchemaSerializer {

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

    private final Collection<Table> _tables = new TreeSet<>();
    private final Collection<Sequence> _seqs = new TreeSet<>();

    /**
     * Constructor. Supply configuration.
     */
    public XMLSchemaSerializer(JDBCConfiguration conf) {
        setLog(conf.getLog(JDBCConfiguration.LOG_SCHEMA));
    }

    @Override
    public Table[] getTables() {
        return (Table[]) _tables.toArray(new Table[_tables.size()]);
    }

    @Override
    public void addTable(Table table) {
        if (table != null)
            _tables.add(table);
    }

    @Override
    public boolean removeTable(Table table) {
        return _tables.remove(table);
    }

    public Sequence[] getSequences() {
        return (Sequence[]) _seqs.toArray(new Sequence[_seqs.size()]);
    }

    public void addSequence(Sequence seq) {
        if (seq != null)
            _seqs.add(seq);
    }

    public boolean removeSequence(Sequence seq) {
        return _seqs.remove(seq);
    }

    @Override
    public void addAll(Schema schema) {
        if (schema == null)
            return;
        Table[] tables = schema.getTables();
        for (Table table : tables) {
            addTable(table);
        }
        Sequence[] seqs = schema.getSequences();
        for (Sequence seq : seqs) {
            addSequence(seq);
        }
    }

    @Override
    public void addAll(SchemaGroup group) {
        if (group == null)
            return;
        Schema[] schemas = group.getSchemas();
        for (Schema schema : schemas) {
            addAll(schema);
        }
    }

    @Override
    public boolean removeAll(Schema schema) {
        if (schema == null)
            return false;

        boolean removed = false;
        Table[] tables = schema.getTables();
        for (Table table : tables) {
            removed |= removeTable(table);
        }
        Sequence[] seqs = schema.getSequences();
        for (Sequence seq : seqs) {
            removed |= removeSequence(seq);
        }
        return removed;
    }

    @Override
    public boolean removeAll(SchemaGroup group) {
        if (group == null)
            return false;

        boolean removed = false;
        Schema[] schemas = group.getSchemas();
        for (Schema schema : schemas) {
            removed |= removeAll(schema);
        }
        return removed;
    }

    @Override
    public void clear() {
        _tables.clear();
        _seqs.clear();
    }

    @Override
    protected Collection getObjects() {
        if (_seqs.isEmpty())
            return _tables;
        if (_tables.isEmpty())
            return _seqs;
        List<Object> all = new ArrayList<>(_seqs.size() + _tables.size());
        all.addAll(_seqs);
        all.addAll(_tables);
        return all;
    }

    @Override
    protected void serialize(Collection objs)
        throws SAXException {
        // group the objects by schema
        Map schemas = new HashMap();
        String schemaName;
        Collection schemaObjs;
        Object obj;
        for (Object value : objs) {
            obj = value;
            if (obj instanceof Table)
                schemaName = ((Table) obj).getSchemaName();
            else
                schemaName = ((Sequence) obj).getSchemaName();
            schemaObjs = (Collection) schemas.get(schemaName);
            if (schemaObjs == null) {
                schemaObjs = new LinkedList();
                schemas.put(schemaName, schemaObjs);
            }
            schemaObjs.add(obj);
        }

        startElement("schemas");
        Map.Entry entry;
        for (Object o : schemas.entrySet()) {
            entry = (Map.Entry) o;
            serializeSchema((String) entry.getKey(), (Collection)
                    entry.getValue());
        }
        endElement("schemas");
    }

    /**
     * Serializes the given objects together into the current schema.
     */
    private void serializeSchema(String name, Collection<?> objs)
        throws SAXException {
        if (objs.isEmpty())
            return;

        if (getLog().isTraceEnabled())
            getLog().trace(_loc.get("ser-schema", name));

        if (name != null)
            addAttribute("name", name);
        startElement("schema");

        // tables and seqs
        Object obj;
        for (Object o : objs) {
            obj = o;
            if (obj instanceof Table)
                serializeTable((Table) obj);
            else
                serializeSequence((Sequence) obj);
        }

        endElement("schema");
    }

    /**
     * Serialize the given sequence.
     */
    private void serializeSequence(Sequence seq)
        throws SAXException {
        addAttribute("name", seq.getName());
        if (seq.getInitialValue() != 1)
            addAttribute("initial-value",
                String.valueOf(seq.getInitialValue()));
        if (seq.getIncrement() > 1)
            addAttribute("increment", String.valueOf(seq.getIncrement()));
        if (seq.getAllocate() > 1)
            addAttribute("allocate", String.valueOf(seq.getAllocate()));
        startElement("sequence");
        endElement("sequence");
    }

    /**
     * Serializes the given table.
     */
    private void serializeTable(Table table)
        throws SAXException {
        addAttribute("name", table.getName());
        startElement("table");

        // primary key
        PrimaryKey pk = table.getPrimaryKey();
        if (pk != null)
            serializePrimaryKey(pk);

        // columns
        Column[] cols = table.getColumns();
        for (Column col : cols) {
            serializeColumn(col);
        }

        // foreign keys
        ForeignKey[] fks = table.getForeignKeys();
        for (ForeignKey fk : fks) {
            serializeForeignKey(fk);
        }

        // indexes
        Index[] idxs = table.getIndexes();
        for (Index idx : idxs) {
            serializeIndex(idx);
        }

        // unique constraints
        Unique[] unqs = table.getUniques();
        for (Unique unq : unqs) {
            serializeUnique(unq);
        }

        endElement("table");
    }

    /**
     * Serializes the given column.
     */
    private void serializeColumn(Column col)
        throws SAXException {
        addAttribute("name", col.getName());
        addAttribute("type", Schemas.getJDBCName(col.getType()));
        if (!StringUtil.isEmpty(col.getTypeName())
            && !col.getTypeName().equalsIgnoreCase
            (Schemas.getJDBCName(col.getType())))
            addAttribute("type-name", col.getTypeName());
        if (col.isNotNull())
            addAttribute("not-null", "true");
        if (col.isAutoAssigned())
            addAttribute("auto-assign", "true");
        if (col.getDefaultString() != null)
            addAttribute("default", col.getDefaultString());
        if (col.getSize() != 0)
            addAttribute("size", String.valueOf(col.getSize()));
        if (col.getDecimalDigits() != 0)
            addAttribute("decimal-digits", String.valueOf
                (col.getDecimalDigits()));
        startElement("column");
        endElement("column");
    }

    /**
     * Serializes the given primary key.
     */
    private void serializePrimaryKey(PrimaryKey pk)
        throws SAXException {
        if (pk.getName() != null)
            addAttribute("name", pk.getName());
        if (pk.isLogical())
            addAttribute("logical", "true");

        Column[] cols = pk.getColumns();
        if (cols.length == 1)
            addAttribute("column", cols[0].getName());
        startElement("pk");

        // columns
        if (cols.length > 1)
            for (Column col : cols) {
                serializeOn(col);
            }

        endElement("pk");
    }

    /**
     * Serializes the given index.
     */
    private void serializeIndex(Index idx)
        throws SAXException {
        addAttribute("name", idx.getName());
        if (idx.isUnique())
            addAttribute("unique", "true");
        Column[] cols = idx.getColumns();
        if (cols.length == 1)
            addAttribute("column", cols[0].getName());
        startElement("index");

        // columns
        if (cols.length > 1)
            for (Column col : cols) {
                serializeOn(col);
            }

        endElement("index");
    }

    /**
     * Serializes the given constraint.
     */
    private void serializeUnique(Unique unq)
        throws SAXException {
        if (unq.getName() != null)
            addAttribute("name", unq.getName());
        if (unq.isDeferred())
            addAttribute("deferred", "true");
        Column[] cols = unq.getColumns();
        if (cols.length == 1)
            addAttribute("column", cols[0].getName());
        startElement("unique");

        // columns
        if (cols.length > 1)
            for (Column col : cols) {
                serializeOn(col);
            }

        endElement("unique");
    }

    /**
     * Serializes the given foreign key.
     */
    private void serializeForeignKey(ForeignKey fk)
        throws SAXException {
        if (fk.getName() != null)
            addAttribute("name", fk.getName());

        if (fk.isDeferred())
            addAttribute("deferred", "true");

        if (fk.getDeleteAction() != ForeignKey.ACTION_NONE)
            addAttribute("delete-action", ForeignKey.getActionName
                (fk.getDeleteAction()));
        if (fk.getUpdateAction() != ForeignKey.ACTION_NONE
            && fk.getUpdateAction() != ForeignKey.ACTION_RESTRICT)
            addAttribute("update-action", ForeignKey.getActionName
                (fk.getUpdateAction()));

        Column[] cols = fk.getColumns();
        Column[] pks = fk.getPrimaryKeyColumns();
        Column[] consts = fk.getConstantColumns();
        Column[] constsPK = fk.getConstantPrimaryKeyColumns();
        addAttribute("to-table", fk.getPrimaryKeyTable().getFullName());
        if (cols.length == 1 && consts.length == 0 && constsPK.length == 0)
            addAttribute("column", cols[0].getName());
        startElement("fk");

        // columns
        if (cols.length > 1 || consts.length > 0 || constsPK.length > 0)
            for (int i = 0; i < cols.length; i++)
                serializeJoin(cols[i], pks[i]);
        for (Column aConst : consts) {
            serializeJoin(aConst, fk.getConstant(aConst));
        }
        for (Column column : constsPK) {
            serializeJoin(fk.getPrimaryKeyConstant(column), column);
        }

        endElement("fk");
    }

    /**
     * Serializes the given column to an 'on' element.
     */
    private void serializeOn(Column col)
        throws SAXException {
        addAttribute("column", col.getName());
        startElement("on");
        endElement("on");
    }

    /**
     * Serializes the given columns to a 'join' element.
     */
    private void serializeJoin(Column col, Column pk)
        throws SAXException {
        addAttribute("column", col.getName());
        addAttribute("to-column", pk.getName());
        startElement("join");
        endElement("join");
    }

    /**
     * Serializes the given values to a 'join' element.
     */
    private void serializeJoin(Object val, Column pk)
        throws SAXException {
        addAttribute("value", stringifyConstant(val));
        addAttribute("to-column", pk.getName());
        startElement("join");
        endElement("join");
    }

    /**
     * Serializes the given values to a 'join' element.
     */
    private void serializeJoin(Column col, Object val)
        throws SAXException {
        addAttribute("column", col.getName());
        addAttribute("value", stringifyConstant(val));
        startElement("join");
        endElement("join");
    }

    /**
     * Stringify the given constant value.
     */
    private static String stringifyConstant(Object val) {
        if (val == null)
            return "null";
        if (val instanceof String)
            return "'" + val + "'";
        return val.toString();
    }
}
