/**
 * Copyright (C) 2009-2013 FoundationDB, LLC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.foundationdb.ais.protobuf;

import com.foundationdb.ais.model.*;
import com.foundationdb.ais.util.TableChange;
import com.foundationdb.server.error.ProtobufWriteException;
import com.google.protobuf.CodedOutputStream;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;

public class ProtobufWriter {
    public static interface WriteSelector {
        Columnar getSelected(Columnar columnar);
        /** Called for any table.getGroup() when getSelected(table) is not null */
        boolean isSelected(Group group);
        /** Called for all parent joins where getSelected(Table) is not null **/
        boolean isSelected(Join parentJoin);
        /** Called for all GroupIndexes and all table indexes where getSelected(Table) is not null **/
        boolean isSelected(Index index);
        boolean isSelected(Sequence sequence);
        boolean isSelected(Routine routine);
        boolean isSelected(SQLJJar sqljJar);
        boolean isSelected(ForeignKey foreignKey);
    }

    public static final WriteSelector ALL_SELECTOR = new WriteSelector() {
        @Override
        public Columnar getSelected(Columnar columnar) {
            return columnar;
        }

        @Override
        public boolean isSelected(Group group) {
            return true;
        }

        @Override
        public boolean isSelected(Join join) {
            return true;
        }

        @Override
        public boolean isSelected(Index index) {
            return true;
        }

        @Override
        public boolean isSelected(Sequence sequence) {
            return true;
        }

        @Override
        public boolean isSelected(Routine routine) {
            return true;
        }

        @Override
        public boolean isSelected(SQLJJar sqljJar) {
            return true;
        }

        @Override
        public boolean isSelected(ForeignKey foreignKey) {
            return true;
        }
    };

    public static abstract class TableFilterSelector implements WriteSelector {
        @Override
        public boolean isSelected(Index index) {
            return true;
        }

        @Override
        public boolean isSelected(Group group) {
            return true;
        }

        @Override
        public boolean isSelected(Join join) {
            return true;
        }

        @Override
        public boolean isSelected(Sequence sequence) {
            return true;
        }

        @Override
        public boolean isSelected(Routine routine) {
            return true;
        }

        @Override
        public boolean isSelected(SQLJJar sqljJar) {
            return true;
        }

        @Override
        public boolean isSelected(ForeignKey foreignKey) {
            return true;
        }
    }

    public static abstract class TableSelector extends TableFilterSelector {
        public abstract boolean isSelected(Columnar columnar);

        @Override
        public Columnar getSelected(Columnar columnar) {
            return isSelected(columnar) ? columnar : null;
        }
    }

    public static class SingleSchemaSelector implements WriteSelector {
        private final String schemaName;

        public SingleSchemaSelector(String schemaName) {
            this.schemaName = schemaName;
        }

        public String getSchemaName() {
            return schemaName;
        }

        @Override
        public Columnar getSelected(Columnar columnar) {
            return schemaName.equals(columnar.getName().getSchemaName()) ? columnar : null;
        }

        @Override
        public boolean isSelected(Group group) {
            return true;
        }

        @Override
        public boolean isSelected(Join join) {
            return true;
        }

        @Override
        public boolean isSelected(Index index) {
            return true;
        }
        
        @Override 
        public boolean isSelected(Sequence sequence) {
            return schemaName.equals(sequence.getSequenceName().getSchemaName());
        }
        
        @Override 
        public boolean isSelected(Routine routine) {
            return schemaName.equals(routine.getName().getSchemaName());
        }

        @Override
        public boolean isSelected(SQLJJar sqljJar) {
            return schemaName.equals(sqljJar.getName().getSchemaName());
        }

        @Override
        public boolean isSelected(ForeignKey foreignKey) {
            return true;
        }
    }


    private AISProtobuf.AkibanInformationSchema pbAIS;
    private final WriteSelector selector;


    public ProtobufWriter() {
        this(ALL_SELECTOR);
    }

    public ProtobufWriter(WriteSelector selector) {
        this.selector = selector;
    }

    /** Convert the given AIS into Protobuf classes. Returned instance also available via {@link #getPBAIS()}. */
    public AISProtobuf.AkibanInformationSchema save(AkibanInformationSchema ais) {
        AISProtobuf.AkibanInformationSchema.Builder aisBuilder = AISProtobuf.AkibanInformationSchema.newBuilder();

        if(selector instanceof SingleSchemaSelector) {
            Schema schema = ais.getSchema(((SingleSchemaSelector) selector).getSchemaName());
            if(schema != null) {
                writeSchema(aisBuilder, schema, selector);
            }
        } else {
            for(Schema schema : ais.getSchemas().values()) {
                writeSchema(aisBuilder, schema, selector);
            }
        }

        pbAIS = aisBuilder.build();
        return pbAIS;
    }

    public AISProtobuf.AkibanInformationSchema getPBAIS() {
        return pbAIS;
    }

    /** Size buffer needs for calling {@link #serialize} */
    public int getBufferSize() {
        assert pbAIS != null;
        return pbAIS.getSerializedSize() + 4; // + int
    }

    /** Serialize into <code>buffer</code>. Buffer must have array and enough space, see {@link #getBufferSize}. */
    public void serialize(ByteBuffer buffer) {
        assert pbAIS != null;
        assert buffer.hasArray() : "Array required";
        String MESSAGE_NAME = AISProtobuf.AkibanInformationSchema.getDescriptor().getFullName();
        int requiredSize = getBufferSize();
        int serializedSize = requiredSize - 4; // Added by requiredBufferSize
        int bufferSize = buffer.limit() - buffer.position();
        if(bufferSize < requiredSize) {
            throw new ProtobufWriteException(MESSAGE_NAME, "Required size exceeds buffer size");
        }
        buffer.putInt(serializedSize);
        int bufferPos = buffer.position();
        bufferSize = buffer.limit() - bufferPos;
        CodedOutputStream codedOutput = CodedOutputStream.newInstance(buffer.array(), bufferPos, bufferSize);
        try {
            pbAIS.writeTo(codedOutput);
            // Successfully written, update backing buffer info
            buffer.position(bufferPos + serializedSize);
        } catch(IOException e) {
            // CodedOutputStream really only throws OutOfSpace exception, but declares IOE
            throw new ProtobufWriteException(MESSAGE_NAME, e.getMessage());
        }
    }

    private static void writeSchema(AISProtobuf.AkibanInformationSchema.Builder aisBuilder, Schema schema, WriteSelector selector) {
        AISProtobuf.Schema.Builder schemaBuilder = AISProtobuf.Schema.newBuilder();
        schemaBuilder.setSchemaName(schema.getName());
        // .setCharColl(convertCharAndCol(schema.getCharsetName(), schema.getCollationName()));
        boolean isEmpty = true;

        // Write groups into same schema as root table
        for(Table table : schema.getTables().values()) {
            table = (Table)selector.getSelected(table);
            if(table != null) {
                Group group = table.getGroup();
                if((table.getParentJoin() == null) && (group != null) && selector.isSelected(group)) {
                    writeGroup(schemaBuilder, table.getGroup(), selector);
                }
                writeTable(schemaBuilder, table, selector);
                isEmpty = false;
            }
        }
        
        for(View view : schema.getViews().values()) {
            view = (View)selector.getSelected(view);
            if(view != null) {
                writeView(schemaBuilder, view);
                isEmpty = false;
            }
        }

        for (Sequence sequence : schema.getSequences().values()) {
            if (selector.isSelected (sequence)) { 
                writeSequence(schemaBuilder, sequence);
                isEmpty = false;
            }
        }

        for (Routine routine : schema.getRoutines().values()) {
            if (selector.isSelected(routine)) { 
                writeRoutine(schemaBuilder, routine);
                isEmpty = false;
            }
        }

        for (SQLJJar sqljJar : schema.getSQLJJars().values()) {
            if (selector.isSelected(sqljJar)) { 
                writeSQLJJar(schemaBuilder, sqljJar);
                isEmpty = false;
            }
        }

        if(!isEmpty) {
            aisBuilder.addSchemas(schemaBuilder.build());
        }
    }

    private static void writeGroup(AISProtobuf.Schema.Builder schemaBuilder, Group group, WriteSelector selector) {
        final Table rootTable = group.getRoot();
        AISProtobuf.Group.Builder groupBuilder = AISProtobuf.Group.newBuilder().
                setRootTableName(rootTable.getName().getTableName());
        if(group.getStorageDescription() != null) {
            AISProtobuf.Storage.Builder storageBuilder = AISProtobuf.Storage.newBuilder();
            group.getStorageDescription().writeProtobuf(storageBuilder);
            groupBuilder.setStorage(storageBuilder.build());
        }

        for(Index index : group.getIndexes()) {
            if(selector.isSelected(index)) {
                writeGroupIndex(groupBuilder, index);
            }
        }

        schemaBuilder.addGroups(groupBuilder.build());
    }

    private static void writeTable(AISProtobuf.Schema.Builder schemaBuilder, Table table, WriteSelector selector) {
        AISProtobuf.Table.Builder tableBuilder = AISProtobuf.Table.newBuilder();
        tableBuilder
            .setTableName(table.getName().getTableName())
            .setTableId(table.getTableId());
        
        AISProtobuf.CharCollation cac = convertCharAndCol(table.getCharsetName(),
                                                          table.getCollationName());
        if (cac != null) {
            tableBuilder.setCharColl(cac);
        }

        if(table.getOrdinal() != null) {
            tableBuilder.setOrdinal(table.getOrdinal());
        }

        AISProtobuf.UUID tableUuid = convertUUID(table.getUuid());
        if (tableUuid != null) {
            tableBuilder.setUuid(tableUuid);
        }

        if(table.hasVersion()) {
            tableBuilder.setVersion(table.getVersion());
        }

        for(Column column : table.getColumnsIncludingInternal()) {
            writeColumn(tableBuilder, column);
        }

        for(Index index : table.getIndexesIncludingInternal()) {
            if(selector.isSelected(index)) {
                writeTableIndex(tableBuilder, index);
            }
        }

        Join join = table.getParentJoin();
        if((join != null) && selector.isSelected(join)) {
            final Table parent = join.getParent();
            AISProtobuf.Join.Builder joinBuilder = AISProtobuf.Join.newBuilder();
            TableName joinConstraintName = join.getConstraintName();
            joinBuilder.setConstraintName(AISProtobuf.TableName.newBuilder().
                    setSchemaName(joinConstraintName.getSchemaName()).setTableName(joinConstraintName.getTableName())).
                    setParentTable(AISProtobuf.TableName.newBuilder().
                    setSchemaName(parent.getName().getSchemaName()).
                    setTableName(parent.getName().getTableName()).
                    build());

            int position = 0;
            for(JoinColumn joinColumn : join.getJoinColumns()) {
                joinBuilder.addColumns(AISProtobuf.JoinColumn.newBuilder().
                        setParentColumn(joinColumn.getParent().getName()).
                        setChildColumn(joinColumn.getChild().getName()).
                        setPosition(position++).
                        build());
            }

            tableBuilder.setParentTable(joinBuilder.build());
        }

        if (table.getPendingOSC() != null) {
            writePendingOSC(tableBuilder, table.getPendingOSC());
        }

        for(FullTextIndex index : table.getOwnFullTextIndexes()) {
            if(selector.isSelected(index)) {
                writeFullTextIndex(tableBuilder, index);
            }
        }

        for(ForeignKey fk : table.getReferencingForeignKeys()) {
            if(selector.isSelected(fk)) {
                writeForeignKey(tableBuilder, fk);
            }
        }

        schemaBuilder.addTables(tableBuilder.build());
    }

    private static void writeColumn(AISProtobuf.Table.Builder tableBuilder, Column column) {
        tableBuilder.addColumns(writeColumnCommon(column));
    }

    private static void writeView(AISProtobuf.Schema.Builder schemaBuilder, View view) {
        AISProtobuf.View.Builder viewBuilder = AISProtobuf.View.newBuilder();
        viewBuilder.
                setViewName(view.getName().getTableName()).
                setDefinition(view.getDefinition());
               // Not yet in AIS: description, protected

        for(Column column : view.getColumnsIncludingInternal()) {
            writeColumn(viewBuilder, column);
        }

        for(String key : view.getDefinitionProperties().stringPropertyNames()) {
            String value = view.getDefinitionProperties().getProperty(key);
            viewBuilder.addDefinitionProperties(AISProtobuf.Property.newBuilder().
                                                setKey(key).setValue(value).
                                                build());
        }
        
        for(Map.Entry<TableName,Collection<String>> entry : view.getTableColumnReferences().entrySet()) {
            AISProtobuf.ColumnReference.Builder referenceBuilder = 
                AISProtobuf.ColumnReference.newBuilder().
                setTable(AISProtobuf.TableName.newBuilder().
                         setSchemaName(entry.getKey().getSchemaName()).
                         setTableName(entry.getKey().getTableName()).
                         build());
            for (String column : entry.getValue()) {
                referenceBuilder.addColumns(column);
            }
            viewBuilder.addReferences(referenceBuilder.build());
        }

        schemaBuilder.addViews(viewBuilder.build());
    }

    private static void writeColumn(AISProtobuf.View.Builder viewBuilder, Column column) {
        viewBuilder.addColumns(writeColumnCommon(column));
    }

    private static AISProtobuf.Column writeColumnCommon(Column column) {
        AISProtobuf.Column.Builder columnBuilder = AISProtobuf.Column.newBuilder().
                setColumnName(column.getName()).
                setTypeName(column.getTypeName()).
                setTypeBundleUUID(convertUUID(column.getTypeBundleUUID())).
                setTypeVersion(column.getTypeVersion()).
                setIsNullable(column.getNullable()).
                setPosition(column.getPosition());

        if(column.hasCharsetAndCollation()) {
            columnBuilder.setCharColl(convertCharAndCol(column.getCharsetName(), column.getCollationName()));
        }

        AISProtobuf.UUID columnUuid = convertUUID(column.getUuid());
        if (columnUuid != null) {
            columnBuilder.setUuid(columnUuid);
        }

        if(column.getTypeParameter1() != null) {
            columnBuilder.setTypeParam1(column.getTypeParameter1());
        }
        if(column.getTypeParameter2() != null) {
            columnBuilder.setTypeParam2(column.getTypeParameter2());
        }
        
        if (column.getDefaultIdentity() != null) {
            columnBuilder.setDefaultIdentity (column.getDefaultIdentity());
        }
        
        if (column.getIdentityGenerator() != null) {
            columnBuilder.setSequence(AISProtobuf.TableName.newBuilder()
                    .setSchemaName(column.getIdentityGenerator().getSequenceName().getSchemaName())
                    .setTableName(column.getIdentityGenerator().getSequenceName().getTableName())
                    .build());
        }
        Long maxStorage = column.getMaxStorageSizeWithoutComputing();
        if(maxStorage != null) {
            columnBuilder.setMaxStorageSize(maxStorage);
        }
        Integer prefix = column.getPrefixSizeWithoutComputing();
        if(prefix != null) {
            columnBuilder.setPrefixSize(prefix);
        }
        if(column.getDefaultValue() != null) {
            columnBuilder.setDefaultValue(column.getDefaultValue());
        }
        if(column.getDefaultFunction() != null) {
            columnBuilder.setDefaultFunction(column.getDefaultFunction());
        }
        return columnBuilder.build();
    }

    private static AISProtobuf.Index writeIndexCommon(Index index, boolean withTableName) {
        final IndexName indexName = index.getIndexName();
        AISProtobuf.Index.Builder indexBuilder = AISProtobuf.Index.newBuilder();
        AISProtobuf.TableName.Builder tableNameBuilder = AISProtobuf.TableName.newBuilder();
        AISProtobuf.TableName constraintName;
        indexBuilder.
                setIndexName(indexName.getName()).
                setIndexId(index.getIndexId()).
                setIsPK(index.isPrimaryKey()).
                setIsUnique(index.isUnique()).
                setIndexMethod(convertIndexMethod(index.getIndexMethod()));

        if (index.getConstraintName() != null) {
            tableNameBuilder.
                    setSchemaName(index.getConstraintName().getSchemaName()).
                    setTableName(index.getConstraintName().getTableName());
            constraintName = tableNameBuilder.build();
            indexBuilder.setConstraintName(constraintName);
        }
        // Not yet in AIS: description        
        if(index.isGroupIndex()) {
            indexBuilder.setJoinType(convertJoinType(index.getJoinType()));
        }
        if(index.getStorageDescription() != null) {
            AISProtobuf.Storage.Builder storageBuilder = AISProtobuf.Storage.newBuilder();
            index.getStorageDescription().writeProtobuf(storageBuilder);
            indexBuilder.setStorage(storageBuilder.build());
        }
        if (index.getIndexMethod() == Index.IndexMethod.GEO_LAT_LON ||
            index.getIndexMethod() == Index.IndexMethod.GEO_WKB ||
            index.getIndexMethod() == Index.IndexMethod.GEO_WKT) {
            indexBuilder.
                    setFirstSpatialArg(index.firstSpatialArgument()).
                    setLastSpatialArg(index.lastSpatialArgument()).
                    setDimensions(index.dimensions());

        }

        for(IndexColumn indexColumn : index.getKeyColumns()) {
            writeIndexColumn(indexBuilder, indexColumn, withTableName);
        }

        return indexBuilder.build();
    }

    private static void writeTableIndex(AISProtobuf.Table.Builder tableBuilder, Index index) {
        tableBuilder.addIndexes(writeIndexCommon(index, false));
    }

    private static void writeGroupIndex(AISProtobuf.Group.Builder groupBuilder, Index index) {
        groupBuilder.addIndexes(writeIndexCommon(index, true));
    }

    private static void writeFullTextIndex(AISProtobuf.Table.Builder tableBuilder, FullTextIndex index) {
        tableBuilder.addFullTextIndexes(writeIndexCommon(index, true));
    }

    private static void writeIndexColumn(AISProtobuf.Index.Builder indexBuilder, IndexColumn indexColumn, boolean withTableName) {
        AISProtobuf.IndexColumn.Builder indexColumnBuilder = AISProtobuf.IndexColumn.newBuilder().
                setColumnName(indexColumn.getColumn().getName()).
                setIsAscending(indexColumn.isAscending()).
                setPosition(indexColumn.getPosition());

        if(indexColumn.getIndexedLength() != null) {
            indexColumnBuilder.setIndexedLength(indexColumn.getIndexedLength());
        }
        
        if(withTableName) {
            TableName tableName = indexColumn.getColumn().getTable().getName();
            indexColumnBuilder.setTableName(
                    AISProtobuf.TableName.newBuilder().
                            setSchemaName(tableName.getSchemaName()).
                            setTableName(tableName.getTableName()).
                            build()
            );
        }

        indexBuilder.addColumns(indexColumnBuilder.build());
    }

    private static AISProtobuf.UUID convertUUID(UUID uuid) {
        if (uuid == null)
            return null;
        return AISProtobuf.UUID.newBuilder().
            setMostSignificantBits(uuid.getMostSignificantBits()).
            setLeastSignificantBits(uuid.getLeastSignificantBits()).
            build();
    }

    private static AISProtobuf.JoinType convertJoinType(Index.JoinType joinType) {
        switch(joinType) {
            case LEFT: return AISProtobuf.JoinType.LEFT_OUTER_JOIN;
            case RIGHT: return AISProtobuf.JoinType.RIGHT_OUTER_JOIN;
        }
        throw new ProtobufWriteException(AISProtobuf.Join.getDescriptor().getFullName(),
                                         "No match for Index.JoinType "+joinType.name());
    }

    private static AISProtobuf.CharCollation convertCharAndCol(String charset, String collation) {
        if ((charset == null) && (collation == null))
            return null;
        return AISProtobuf.CharCollation.newBuilder().
                setCharacterSetName(charset).
                setCollationOrderName(collation).
                build();
    }
    
    private static AISProtobuf.IndexMethod convertIndexMethod(Index.IndexMethod indexMethod) {
        switch (indexMethod) {
        case NORMAL: 
        default:
            return AISProtobuf.IndexMethod.NORMAL;
        case GEO_LAT_LON:
            return AISProtobuf.IndexMethod.GEO_LAT_LON;
        case GEO_WKB:
            return AISProtobuf.IndexMethod.GEO_WKB;
        case GEO_WKT:
            return AISProtobuf.IndexMethod.GEO_WKT;
        case FULL_TEXT:
            return AISProtobuf.IndexMethod.FULL_TEXT;
        }
    }

    private static void writePendingOSC(AISProtobuf.Table.Builder tableBuilder, PendingOSC pendingOSC) {
        AISProtobuf.PendingOSC.Builder oscBuilder = AISProtobuf.PendingOSC.newBuilder();
        oscBuilder.setOriginalName(pendingOSC.getOriginalName());
        for (TableChange columnChange : pendingOSC.getColumnChanges()) {
            oscBuilder.addColumnChanges(writePendingOSChange(columnChange));
        }
        for (TableChange indexChange : pendingOSC.getIndexChanges()) {
            oscBuilder.addIndexChanges(writePendingOSChange(indexChange));
        }
        if (pendingOSC.getCurrentName() != null)
            oscBuilder.setCurrentName(pendingOSC.getCurrentName());
        tableBuilder.setPendingOSC(oscBuilder.build());
    }

    private static AISProtobuf.PendingOSChange writePendingOSChange(TableChange tableChange) {
        AISProtobuf.PendingOSChange.Builder oscBuilder = AISProtobuf.PendingOSChange.newBuilder();
        switch (tableChange.getChangeType()) {
        case ADD:
            oscBuilder.setType(AISProtobuf.PendingOSChangeType.ADD);
            oscBuilder.setNewName(tableChange.getNewName());
            break;
        case DROP:
            oscBuilder.setType(AISProtobuf.PendingOSChangeType.DROP);
            oscBuilder.setOldName(tableChange.getOldName());
            break;
        case MODIFY:
            oscBuilder.setType(AISProtobuf.PendingOSChangeType.MODIFY);
            oscBuilder.setOldName(tableChange.getOldName());
            oscBuilder.setNewName(tableChange.getNewName());
            break;
        }
        return oscBuilder.build();
    }

    private static void writeForeignKey(AISProtobuf.Table.Builder tableBuilder, ForeignKey fk) {
        AISProtobuf.ForeignKey.Builder fkBuilder = AISProtobuf.ForeignKey.newBuilder();
        fkBuilder
            .setConstraintName(fk.getConstraintName().getTableName())   
            .setReferencedTable(AISProtobuf.TableName.newBuilder().
                    setSchemaName(fk.getReferencedTable().getName().getSchemaName()).
                    setTableName(fk.getReferencedTable().getName().getTableName()).
                    build());

        for(Column fkColumn : fk.getReferencingColumns()) {
            fkBuilder.addReferencingColumns(fkColumn.getName());
        }
        for(Column fkColumn : fk.getReferencedColumns()) {
            fkBuilder.addReferencedColumns(fkColumn.getName());
        }

        fkBuilder.setOnDelete(convertForeignKeyAction(fk.getDeleteAction()));
        fkBuilder.setOnUpdate(convertForeignKeyAction(fk.getUpdateAction()));
        fkBuilder.setDeferrable(fk.isDeferrable());
        fkBuilder.setInitiallyDeferred(fk.isInitiallyDeferred());

        tableBuilder.addForeignKeys(fkBuilder.build());
    }

    private static AISProtobuf.ForeignKeyAction convertForeignKeyAction(ForeignKey.Action action) {
        switch (action) {
        case NO_ACTION:
        default:
            return AISProtobuf.ForeignKeyAction.NO_ACTION;
        case RESTRICT:
            return AISProtobuf.ForeignKeyAction.RESTRICT;
        case CASCADE:
            return AISProtobuf.ForeignKeyAction.CASCADE;
        case SET_NULL:
            return AISProtobuf.ForeignKeyAction.SET_NULL;
        case SET_DEFAULT:
            return AISProtobuf.ForeignKeyAction.SET_DEFAULT;
        }
    }

    private static void writeSequence (AISProtobuf.Schema.Builder schemaBuilder, Sequence sequence) {
        AISProtobuf.Sequence.Builder sequenceBuilder = AISProtobuf.Sequence.newBuilder()
                .setSequenceName(sequence.getSequenceName().getTableName())
                .setStart(sequence.getStartsWith())
                .setIncrement(sequence.getIncrement())
                .setMinValue(sequence.getMinValue())
                .setMaxValue(sequence.getMaxValue())
                .setIsCycle(sequence.isCycle());
        if(sequence.getStorageDescription() != null) {
            AISProtobuf.Storage.Builder storageBuilder = AISProtobuf.Storage.newBuilder();
            sequence.getStorageDescription().writeProtobuf(storageBuilder);
            sequenceBuilder.setStorage(storageBuilder.build());
        }
        schemaBuilder.addSequences (sequenceBuilder.build());
    }

    private static void writeRoutine(AISProtobuf.Schema.Builder schemaBuilder, Routine routine) {
        AISProtobuf.Routine.Builder routineBuilder = AISProtobuf.Routine.newBuilder()
            .setRoutineName(routine.getName().getTableName())
            .setLanguage(routine.getLanguage())
            .setCallingConvention(convertRoutineCallingConvention(routine.getCallingConvention()));
        for (Parameter parameter : routine.getParameters()) {
            writeParameter(routineBuilder, parameter);
        }
        if (routine.getReturnValue() != null) {
            writeParameter(routineBuilder, routine.getReturnValue());
        }
        SQLJJar sqljJar = routine.getSQLJJar();
        if (sqljJar != null) {
            routineBuilder.setJarName(AISProtobuf.TableName.newBuilder()
                                      .setSchemaName(sqljJar.getName().getSchemaName())
                                      .setTableName(sqljJar.getName().getTableName())
                                      .build());
        }
        if (routine.getClassName() != null)
            routineBuilder.setClassName(routine.getClassName());
        if (routine.getMethodName() != null)
            routineBuilder.setMethodName(routine.getMethodName());
        if (routine.getDefinition() != null)
            routineBuilder.setDefinition(routine.getDefinition());
        if (routine.getSQLAllowed() != null)
            routineBuilder.setSqlAllowed(convertRoutineSQLAllowed(routine.getSQLAllowed()));
        if (routine.getDynamicResultSets() > 0)
            routineBuilder.setDynamicResultSets(routine.getDynamicResultSets());
        if (routine.isDeterministic())
            routineBuilder.setDeterministic(routine.isDeterministic());
        if (routine.isCalledOnNullInput())
            routineBuilder.setCalledOnNullInput(routine.isCalledOnNullInput());
        if (routine.getVersion() > 0)
            routineBuilder.setVersion(routine.getVersion());
        schemaBuilder.addRoutines(routineBuilder.build());
    }

    private static void writeParameter(AISProtobuf.Routine.Builder routineBuilder, Parameter parameter) {
        AISProtobuf.Parameter.Builder parameterBuilder = AISProtobuf.Parameter.newBuilder()
            .setDirection(convertParameterDirection(parameter.getDirection()))
            .setTypeName(parameter.getTypeName())
            .setTypeBundleUUID(convertUUID(parameter.getTypeBundleUUID()))
            .setTypeVersion(parameter.getTypeVersion());
        if (parameter.getTypeParameter1() != null) {
            parameterBuilder.setTypeParam1(parameter.getTypeParameter1());
        }
        if (parameter.getTypeParameter2() != null) {
            parameterBuilder.setTypeParam2(parameter.getTypeParameter2());
        }
        if (parameter.getName() != null) {
            parameterBuilder.setParameterName(parameter.getName());
        }
        routineBuilder.addParameters(parameterBuilder.build());
    }

    private static AISProtobuf.RoutineCallingConvention convertRoutineCallingConvention(Routine.CallingConvention callingConvention) {
        switch (callingConvention) {
        case JAVA:
        default:
            return AISProtobuf.RoutineCallingConvention.JAVA;
        case LOADABLE_PLAN:
            return AISProtobuf.RoutineCallingConvention.LOADABLE_PLAN;
        case SQL_ROW: 
            return AISProtobuf.RoutineCallingConvention.SQL_ROW;
        case SCRIPT_FUNCTION_JAVA: 
            return AISProtobuf.RoutineCallingConvention.SCRIPT_FUNCTION_JAVA;
        case SCRIPT_BINDINGS: 
            return AISProtobuf.RoutineCallingConvention.SCRIPT_BINDINGS;
        case SCRIPT_FUNCTION_JSON: 
            return AISProtobuf.RoutineCallingConvention.SCRIPT_FUNCTION_JSON;
        case SCRIPT_BINDINGS_JSON: 
            return AISProtobuf.RoutineCallingConvention.SCRIPT_BINDINGS_JSON;
        case SCRIPT_LIBRARY: 
            return AISProtobuf.RoutineCallingConvention.SCRIPT_LIBRARY;
        }
    }

    private static AISProtobuf.RoutineSQLAllowed convertRoutineSQLAllowed(Routine.SQLAllowed sqlAllowed) {
        switch (sqlAllowed) {
        case MODIFIES_SQL_DATA:
        default:
            return AISProtobuf.RoutineSQLAllowed.MODIFIES_SQL_DATA;
        case READS_SQL_DATA:
            return AISProtobuf.RoutineSQLAllowed.READS_SQL_DATA;
        case CONTAINS_SQL:
            return AISProtobuf.RoutineSQLAllowed.CONTAINS_SQL;
        case NO_SQL:
            return AISProtobuf.RoutineSQLAllowed.NO_SQL;
        }
    }

    private static AISProtobuf.ParameterDirection convertParameterDirection(Parameter.Direction parameterDirection) {
        switch (parameterDirection) {
        case IN:
        default:
            return AISProtobuf.ParameterDirection.IN;
        case OUT:
            return AISProtobuf.ParameterDirection.OUT;
        case INOUT:
            return AISProtobuf.ParameterDirection.INOUT;
        case RETURN:
            return AISProtobuf.ParameterDirection.RETURN;
        }
    }

    private static void writeSQLJJar(AISProtobuf.Schema.Builder schemaBuilder, SQLJJar sqljJar) {
        AISProtobuf.SQLJJar.Builder jarBuilder = AISProtobuf.SQLJJar.newBuilder()
            .setJarName(sqljJar.getName().getTableName())
            .setUrl(sqljJar.getURL().toExternalForm());
        if (sqljJar.getVersion() > 0)
            jarBuilder.setVersion(sqljJar.getVersion());
        schemaBuilder.addSqljJars(jarBuilder.build());
    }

}
