/*
 * 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.clerezza.rdf.core.sparql.query.impl;

import java.util.List;
import java.util.Set;
import org.apache.clerezza.commons.rdf.BlankNode;
import org.apache.clerezza.commons.rdf.RDFTerm;
import org.apache.clerezza.commons.rdf.IRI;
import org.apache.clerezza.rdf.core.sparql.StringQuerySerializer;
import org.apache.clerezza.rdf.core.sparql.query.AlternativeGraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.AskQuery;
import org.apache.clerezza.rdf.core.sparql.query.BasicGraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.BinaryOperation;
import org.apache.clerezza.rdf.core.sparql.query.BuiltInCall;
import org.apache.clerezza.rdf.core.sparql.query.ConstructQuery;
import org.apache.clerezza.rdf.core.sparql.query.DataSet;
import org.apache.clerezza.rdf.core.sparql.query.DescribeQuery;
import org.apache.clerezza.rdf.core.sparql.query.Expression;
import org.apache.clerezza.rdf.core.sparql.query.FunctionCall;
import org.apache.clerezza.rdf.core.sparql.query.GraphGraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.GraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.GroupGraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.LiteralExpression;
import org.apache.clerezza.rdf.core.sparql.query.OptionalGraphPattern;
import org.apache.clerezza.rdf.core.sparql.query.OrderCondition;
import org.apache.clerezza.rdf.core.sparql.query.ResourceOrVariable;
import org.apache.clerezza.rdf.core.sparql.query.SelectQuery;
import org.apache.clerezza.rdf.core.sparql.query.TriplePattern;
import org.apache.clerezza.rdf.core.sparql.query.UnaryOperation;
import org.apache.clerezza.rdf.core.sparql.query.UriRefExpression;
import org.apache.clerezza.rdf.core.sparql.query.Variable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class implements abstract methods of {@link StringQuerySerializer}
 * to serialize specific {@link Query} types.
 *
 * @author hasan
 */
public class SimpleStringQuerySerializer extends StringQuerySerializer {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String serialize(SelectQuery selectQuery) {
        StringBuffer s = new StringBuffer("SELECT ");
        if (selectQuery.isDistinct()) {
            s.append("DISTINCT\n");
        }
        if (selectQuery.isReduced()) {
            s.append("REDUCED\n");
        }
        if (selectQuery.isSelectAll()) {
            s.append("*");
        } else {
            for (Variable v : selectQuery.getSelection()) {
                appendVariable(s, v);
                s.append(" ");
            }
        }
        s.append("\n");

        appendDataSet(s, (SimpleQuery) selectQuery);
        appendWhere(s, (SimpleQuery) selectQuery);
        appendModifier(s, (SimpleQueryWithSolutionModifier) selectQuery);

        return s.toString();
    }

    private void appendVariable(StringBuffer s, Variable v) {
        s.append("?").append(v.getName());
    }

    private void appendDataSet(StringBuffer s, SimpleQuery q) {
        DataSet dataSet = q.getDataSet();
        if (dataSet != null) {
            for (IRI dg : dataSet.getDefaultGraphs()) {
                s.append("FROM ").append(dg.toString()).append("\n");
            }
            for (IRI ng : dataSet.getNamedGraphs()) {
                s.append("FROM NAMED ").append(ng.toString()).append("\n");
            }
        }
    }

    private void appendWhere(StringBuffer s, SimpleQuery q) {
        GroupGraphPattern queryPattern = q.getQueryPattern();
        if (queryPattern == null) {
            return;
        }
        s.append("WHERE\n");
        appendGroupGraphPattern(s, q.getQueryPattern());
    }

    private void appendGroupGraphPattern(StringBuffer s,
            GroupGraphPattern groupGraphPattern) {

        s.append("{ ");
        for (GraphPattern graphPattern : groupGraphPattern.getGraphPatterns()) {
            appendGraphPattern(s, graphPattern);
        }
        for (Expression e : groupGraphPattern.getFilter()) {
            boolean brackettedExpr = !((e instanceof BuiltInCall)
                    || (e instanceof FunctionCall));
            s.append("FILTER ");
            if (brackettedExpr) {
                s.append("(");
            }
            appendExpression(s, e);
            if (brackettedExpr) {
                s.append(")");
            }
            s.append("\n");
        }
        s.append("} ");
    }

    private void appendGraphPattern(StringBuffer s, GraphPattern graphPattern) {
        if (graphPattern instanceof BasicGraphPattern) {
            appendTriplePatterns(s,
                    ((BasicGraphPattern) graphPattern).getTriplePatterns());
        } else if (graphPattern instanceof GroupGraphPattern) {
            appendGroupGraphPattern(s, (GroupGraphPattern) graphPattern);
        } else if (graphPattern instanceof OptionalGraphPattern) {
            appendGraphPattern(s,
                    ((OptionalGraphPattern) graphPattern).getMainGraphPattern());
            s.append(" OPTIONAL ");
            appendGroupGraphPattern(s,
                    ((OptionalGraphPattern) graphPattern).getOptionalGraphPattern());
        } else if (graphPattern instanceof AlternativeGraphPattern) {
            List<GroupGraphPattern> alternativeGraphPatterns =
                    ((AlternativeGraphPattern) graphPattern).getAlternativeGraphPatterns();
            if ((alternativeGraphPatterns != null) &&
                    (!alternativeGraphPatterns.isEmpty())) {
                appendGroupGraphPattern(s, alternativeGraphPatterns.get(0));
                int size = alternativeGraphPatterns.size();
                int i = 1;
                while (i < size) {
                    s.append(" UNION ");
                    appendGroupGraphPattern(s, alternativeGraphPatterns.get(i));
                    i++;
                }
            }
        } else if (graphPattern instanceof GraphGraphPattern) {
            s.append("ImmutableGraph ");
            appendResourceOrVariable(s, ((GraphGraphPattern) graphPattern).getGraph());
            s.append(" ");
            appendGroupGraphPattern(s, ((GraphGraphPattern) graphPattern).getGroupGraphPattern());
        } else {
            logger.warn("Unsupported GraphPattern {}", graphPattern.getClass());
        }
    }

    private void appendTriplePatterns(StringBuffer s,
            Set<TriplePattern> triplePatterns) {

        for (TriplePattern p : triplePatterns) {
            appendResourceOrVariable(s, p.getSubject());
            s.append(" ");
            appendResourceOrVariable(s, p.getPredicate());
            s.append(" ");
            appendResourceOrVariable(s, p.getObject());
            s.append(" .\n");
        }
    }

    private void appendResourceOrVariable(StringBuffer s, ResourceOrVariable n) {
        if (n.isVariable()) {
            appendVariable(s, n.getVariable());
        } else {
            RDFTerm r = n.getResource();
            if (r instanceof BlankNode) {
                s.append("_:").append(r.toString().replace("@", "."));
            } else {
                s.append(r.toString());
            }
        }
    }

    private void appendExpression(StringBuffer s, Expression e) {
        if (e instanceof Variable) {
            appendVariable(s, (Variable) e);
        } else if (e instanceof BinaryOperation) {
            BinaryOperation bo = (BinaryOperation) e;
            s.append("(");
            appendExpression(s, bo.getLhsOperand());
            s.append(") ").append(bo.getOperatorString()).append(" (");
            appendExpression(s, bo.getRhsOperand());
            s.append(")");
        } else if (e instanceof UnaryOperation) {
            UnaryOperation uo = (UnaryOperation) e;
            s.append(uo.getOperatorString()).append(" (");
            appendExpression(s, uo.getOperand());
            s.append(")");
        } else if (e instanceof BuiltInCall) {
            BuiltInCall b = (BuiltInCall) e;
            appendCall(s, b.getName(), b.getArguements());
        } else if (e instanceof FunctionCall) {
            FunctionCall f = (FunctionCall) e;
            appendCall(s, f.getName().getUnicodeString(), f.getArguements());
        } else if (e instanceof LiteralExpression) {
            appendLiteralExpression(s, (LiteralExpression) e);
        } else if (e instanceof UriRefExpression) {
            s.append(((UriRefExpression) e).getUriRef().toString());
        }
    }

    private void appendCall(StringBuffer s, String name, List<Expression> expr) {
        s.append(name).append("(");
        for (Expression e : expr) {
            appendExpression(s, e);
            s.append(",");
        }
        if (expr.isEmpty()) {
            s.append(")");
        } else {
            s.setCharAt(s.length()-1, ')');
        }
    }

    private void appendLiteralExpression(StringBuffer s, LiteralExpression le) {
        s.append(le.getLiteral().toString());
    }

    private void appendModifier(StringBuffer s, SimpleQueryWithSolutionModifier q) {
        List<OrderCondition> orderConditions = q.getOrderConditions();
        if ((orderConditions != null) && (!orderConditions.isEmpty())) {
            s.append("ORDER BY ");
            for (OrderCondition oc : orderConditions) {
                appendOrderCondition(s, oc);
                s.append("\n");
            }
        }
        if (q.getOffset() > 0) {
            s.append("OFFSET ").append(q.getOffset()).append("\n");
        }
        if (q.getLimit() >= 0) {
            s.append("LIMIT ").append(q.getLimit()).append("\n");
        }
    }

    private void appendOrderCondition(StringBuffer s, OrderCondition oc) {
        if (!oc.isAscending()) {
            s.append("DESC(");
        }
        appendExpression(s, oc.getExpression());
        if (!oc.isAscending()) {
            s.append(")");
        }
        s.append(" ");
    }

    @Override
    public String serialize(ConstructQuery constructQuery) {
        StringBuffer s = new StringBuffer("CONSTRUCT\n");
        Set<TriplePattern> triplePatterns = constructQuery.getConstructTemplate();
        s.append("{ ");
        if (triplePatterns != null && !triplePatterns.isEmpty()) {
            appendTriplePatterns(s, triplePatterns);
        }
        s.append("}\n");

        appendDataSet(s, (SimpleQuery) constructQuery);
        appendWhere(s, (SimpleQuery) constructQuery);
        appendModifier(s, (SimpleQueryWithSolutionModifier) constructQuery);

        return s.toString();
    }

    @Override
    public String serialize(DescribeQuery describeQuery) {
        StringBuffer s = new StringBuffer("DESCRIBE\n");

        if (describeQuery.isDescribeAll()) {
            s.append("*");
        } else {
            for (ResourceOrVariable n : describeQuery.getResourcesToDescribe()) {
                appendResourceOrVariable(s, n);
                s.append(" ");
            }
        }
        appendDataSet(s, (SimpleQuery) describeQuery);
        appendWhere(s, (SimpleQuery) describeQuery);
        appendModifier(s, (SimpleQueryWithSolutionModifier) describeQuery);

        return s.toString();
    }

    @Override
    public String serialize(AskQuery askQuery) {
        StringBuffer s = new StringBuffer("ASK\n");
        appendDataSet(s, (SimpleQuery) askQuery);
        appendWhere(s, (SimpleQuery) askQuery);

        return s.toString();
    }
}
