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

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.data.Column;
import org.apache.empire.data.DataType;
import org.apache.empire.data.Record;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.ItemNotFoundException;
import org.apache.empire.exceptions.NotSupportedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DBGeneratedValue
 * Abstract base class for Auto generated values
 * @author doebele
 */
public abstract class DBGeneratedValue extends DBExpr
{
    protected static final Logger log = LoggerFactory.getLogger(DBGeneratedValue.class);

    /**
     * Returns a generated value for a trigger
     * @param table the table on which the trigger 
     * @param triggerName then name of the trigger
     * @param columns the list of columns used by the trigger
     * @return the value generator
     */
    public static DBGeneratedValue byTrigger(DBTable table, String triggerName, DBTableColumn...columns)
    {
        return new DBTriggerGeneratedValue(table, triggerName, columns);
    }

    /**
     * Returns generator for a record value created from other record fields
     * The function template must contain the column names wrapped in square brackets
     * like e.g. "normalize([name])" or "[lastName]+', '+[firstName]"
     * @param table the table for which the value is generated 
     * @return the value generator
     */
    public static DBGeneratedValue byFunction(DBTable table, String functionTemplate)
    {
        return new DBFuncGeneratedValue(table, functionTemplate);
    }
    
    /**
     * DBTriggerGeneratedValue
     * Value is generated by Trigger
     * @author doebele
     */
    public static class DBTriggerGeneratedValue extends DBGeneratedValue
    {
        private final String triggerName;
        private final DBTableColumn[] sourceColumns;
        
        public DBTriggerGeneratedValue(DBRowSet table, String triggerName, DBTableColumn[] sourceColumns)
        {
            super(table.getDatabase());
            // init
            this.triggerName = triggerName;
            this.sourceColumns = sourceColumns;
        }

        @Override
        public boolean isModified(Record record)
        {
            // check for modified source column
            for (int i=0; i<sourceColumns.length; i++)
                if (record.wasModified(sourceColumns[i]))
                    return true;
            return false;
        }

        @Override
        public Object eval(Record record)
        {
            // value unknown after change
            return ObjectUtils.NO_VALUE;
        }

        @Override
        public void addReferencedColumns(Set<DBColumn> list)
        {
            log.warn("No reference columns for trigger {}", this.triggerName);
        }

        @Override
        public void addSQL(DBSQLBuilder sql, long context)
        {
            log.error("Cannot add sql for value generated by trigger {}", this.triggerName);
            throw new NotSupportedException(this, "addSQL");
        }
    }
    
    /**
     * DBFuncGeneratedValue
     * Auto-generates a record value from other record fields
     * The template must contain the column names wrapped in square brackets
     * like e.g. "normalize([name])" or "[lastName]+', '+[firstName]"
     * @author doebele
     */
    public static class DBFuncGeneratedValue extends DBGeneratedValue
    {
        private final List<Object> parts;
        
        public DBFuncGeneratedValue(DBTable table, String template)
        {
            super(table.getDatabase());
            this.parts = new ArrayList<Object>();
            int idx=0;
            while (idx<template.length())
            {
                int nxt=template.indexOf('[', idx);
                if (nxt<0)
                {   // append rest
                    parts.add(template.substring(idx));
                    break;
                }
                // append
                parts.add(template.substring(idx, nxt++));
                // find end
                int end=template.indexOf(']', nxt);
                if (end<0)
                    throw new InvalidArgumentException("template", template);
                // find column
                String name = template.substring(nxt, end);
                DBColumn column = table.getColumn(name);
                if (column==null)
                    throw new ItemNotFoundException(name);
                // found
                parts.add(column);
                idx = end+1;
            }
        }
        
        /*
         * (non-Javadoc)
         * @see org.apache.empire.db.DBGeneratedValue#isModified(org.apache.empire.data.Record)
         */
        @Override
        public boolean isModified(Record record)
        {
            for (Object part : parts)
            {   // check modified
                if ((part instanceof Column) && record.wasModified((Column)part))
                    return true;
            }
            return false;
        }
        
        /*
         * (non-Javadoc)
         * @see org.apache.empire.db.DBGeneratedValue#eval(org.apache.empire.data.Record)
         */
        @Override
        public Object eval(Record record)
        {
            for (Object part : parts)
            {   // check null
                if ((part instanceof Column) && record.isNull((Column)part))
                    return null;
            }
            // build now
            DBSQLBuilder sql = db.getDbms().createSQLBuilder();
            for (Object part : parts)
            {
                if (part instanceof Column)
                {   // append column value
                    Column col = (Column)part;
                    Object value = record.get(col);
                    sql.appendValue(col.getDataType(), value);
                }
                else
                    sql.append(part.toString());
            }
            // done
            return db.getValueExpr(sql.toString(), DataType.UNKNOWN);
        }

        /*
         * (non-Javadoc)
         * @see org.apache.empire.db.DBExpr#addReferencedColumns(java.util.Set)
         */
        @Override
        public void addReferencedColumns(Set<DBColumn> list)
        {
            for (Object part : parts)
            {
                if (part instanceof DBColumn)
                    list.add((DBColumn)part);
            }
        }

        /*
         * (non-Javadoc)
         * @see org.apache.empire.db.DBExpr#addSQL(org.apache.empire.db.DBSQLBuilder, long)
         */
        @Override
        public void addSQL(DBSQLBuilder sql, long context)
        {
            for (Object part : parts)
            {
                if (part instanceof Column)
                {   // append column value
                    DBColumn col = (DBColumn)part;
                    col.addSQL(sql, context);
                }
                else
                    sql.append(part.toString());
            }
        }
    }
    
    protected final DBDatabase db;
    
    public DBGeneratedValue(DBDatabase db)
    {
        this.db = db;
    }

    /**
     * Returns true if the value was modified and thus needs to be be updated in the record
     * @param record the record for which to check
     * @return true if the value was modified and needs to be be updated
     */
    public abstract boolean isModified(Record record);

    /**
     * Evaluates a record and returns the generated value
     * @param record the record for which to evaluate
     * @return the generated value
     */
    public abstract Object eval(Record record);

    /**
     * @return the current DBDatabase object
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T extends DBDatabase> T getDatabase()
    {
        return (T)this.db;
    }
    
}
