package net.sourceforge.mayfly.evaluation;

import net.sourceforge.mayfly.MayflyException;
import net.sourceforge.mayfly.MayflyInternalException;
import net.sourceforge.mayfly.Options;
import net.sourceforge.mayfly.datastore.Cell;
import net.sourceforge.mayfly.datastore.Row;
import net.sourceforge.mayfly.datastore.StringCell;
import net.sourceforge.mayfly.evaluation.expression.SingleColumn;
import net.sourceforge.mayfly.evaluation.select.Evaluator;
import net.sourceforge.mayfly.parser.Location;
import net.sourceforge.mayfly.util.CaseInsensitiveString;
import net.sourceforge.mayfly.util.ImmutableList;

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

/**
 * @internal
 * Mapping from Expression to Cell.
 * 
 * Unlike a {@link Row}, here we have an ordering.  In some contexts
 * (for example, evaluating an expression), the order doesn't matter,
 * and in fact might be constructed haphazardly.
 * 
 * However, when copying data which we'll return in a select, the
 * order here needs to
 * match the order of columns in a table (for "select foo.*"),
 * and even has some user-visible meaning between tables (for "select *"),
 * although in the latter case I'm not sure what the meaning
 * needs to be.
 */
public class ResultRow {
    
    private final ImmutableList<Element> elements;

    public ResultRow(Row row, String table) {
        this(row, table, new Options());
    }

    public ResultRow(Row row, String table, Options options) {
        this.elements = fromRow(row, table, options);
    }

    public ResultRow() {
        this(new ImmutableList());
    }
    
    private ResultRow(ImmutableList elements) {
        this.elements = elements;
    }

    private static ImmutableList<Element> fromRow(Row row, String table,
        Options options) {
        List result = new ArrayList();
        for (Iterator iter = row.columnNames(); iter.hasNext();) {
            CaseInsensitiveString column = (CaseInsensitiveString) iter.next();
            result.add(new Element(
                new SingleColumn(table, column.getString(), options), 
                row.cell(column)));
        }
        return new ImmutableList(result);
    }

    public int size() {
        return elements.size();
    }

    public Element element(int index) {
        return elements.get(index);
    }
    
    public Cell cell(int index) {
        return element(index).value;
    }
    
    public Expression expression(int index) {
        return element(index).expression;
    }

    public Expression findColumn(String columnName) {
        return findColumn(null, null, columnName, Location.UNKNOWN);
    }

    public SingleColumn findColumn(
        String tableOrAlias, String originalTableOrAlias, String columnName,
        Location location) {
        SingleColumn found = findColumnOrNull(tableOrAlias, columnName, location);
        if (found == null) {
            throw new NoColumn(originalTableOrAlias, columnName, location);
        }
        else {
            return found;
        }
    }

    public SingleColumn findColumnOrNull(
        String tableOrAlias, String columnName, Location location) {
        SingleColumn found = null;
        for (Element element : elements) {
            if (element.expression instanceof SingleColumn) {
                SingleColumn candidate = (SingleColumn) element.expression;
                if (candidate.matches(tableOrAlias, columnName)) {
                    if (found != null) {
                        throw new MayflyException(
                            "ambiguous column " + columnName,
                            location);
                    }
                    else {
                        found = candidate;
                    }
                }
            }
        }
        return found;
    }
    
    public Cell findValue(Expression target) {
        Cell result = findValueOrNull(target);
        if (result == null) {
            throw new MayflyInternalException(
                "Where did expression " + target.displayName() + " come from?");
        }
        else {
            return result;
        }
    }

    public Cell findValueOrNull(Expression target) {
        Expression resolved = target.resolve(this, Evaluator.NO_SUBSELECT_NEEDED);
        for (Element element : elements) {
            if (resolved.matches(element.expression)) {
                return element.value;
            }
        }
        return null;
    }

    public Cell findOrEvaluate(Expression expression) {
        Cell result = findValueOrNull(expression);
        if (result != null) {
            return result;
        }
        else {
            return expression.evaluate(this);
        }
    }
    
    public ResultRow withColumn(String tableOrAlias, String columnName, Cell cell) {
        return with(new SingleColumn(tableOrAlias, columnName), cell);
    }

    public ResultRow withColumn(String tableOrAlias, String column, String value) {
        return withColumn(tableOrAlias, column, new StringCell(value));
    }

    public ResultRow with(Expression expression, Cell value) {
        return new ResultRow(elements.with(new Element(expression, value)));
    }
    
    public String debugString() {
        StringBuilder out = new StringBuilder();
        out.append("Result Row:\n");
        for (Element element : elements) {
            out.append("  ");
            out.append(element.expression.displayName());
            out.append(" = ");
            out.append(element.value.displayName());
            out.append("\n");
        }
        return out.toString();
    }
    
    public static class Element {
        public final Expression expression;
        public final Cell value;

        public Element(Expression expression, Cell value) {
            /* This is probably where we want to go eventually.
               But we need to work through things like how
               we convert a Row to a ResultRow (do we even
               need to do that, outside applyAlias, beyond
               the transition phase)?  There are also issues
               with group-by keys.
            if (expression instanceof SingleColumn) {
                // might also want to recurse to make sure all
                // sub-expressions are resolved.  But this will
                // do for now.
                ((SingleColumn)expression).assertIsResolved();
            }
            */
            this.expression = expression;
            this.value = value;
        }

        public SingleColumn column() {
            return (SingleColumn)expression;
        }
        
    }

    /**
     * Return a new row which has all the columns from two input
     * rows.
     */
    public ResultRow combine(ResultRow right) {
        Set leftTableNames = new HashSet();

        List result = new ArrayList();
        Iterator leftIterator = elements.iterator();
        while (leftIterator.hasNext()) {
            Element element = (Element) leftIterator.next();
            leftTableNames.add(new CaseInsensitiveString(
                element.column().tableOrAlias()));
            result.add(element);
        }

        Iterator rightIterator = right.elements.iterator();
        while (rightIterator.hasNext()) {
            Element element = (Element) rightIterator.next();
            String tableOrAlias = element.column().tableOrAlias();
            if (leftTableNames.contains(new CaseInsensitiveString(tableOrAlias))) {
                throw new MayflyException(
                    "duplicate table name or alias " + tableOrAlias);
            }
            result.add(element);
        }

        return new ResultRow(new ImmutableList(result));
    }

    public ImmutableList expressions() {
        List result = new ArrayList();
        for (Iterator iter = elements.iterator(); iter.hasNext();) {
            Element element = (Element) iter.next();
            result.add(element.expression);
        }
        return new ImmutableList(result);
    }

    public ImmutableList expressionsForTable(String aliasOrTable) {
        List found = new ArrayList();
        for (Element element : elements) {
            if (element.expression instanceof SingleColumn) {
                SingleColumn column = (SingleColumn) element.expression;
                if (column.matchesAliasOrTable(aliasOrTable)) {
                    found.add(column);
                }
            }
        }
        return new ImmutableList(found);
    }

}
