/*
 * 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.cassandra.cql3.functions;

import java.util.List;

import com.google.common.collect.ArrayListMultimap;

import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.AssignementTestable;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;

public abstract class Functions
{
    private Functions() {}

    // If we ever allow this to be populated at runtime, this will need to be thread safe.
    private static final ArrayListMultimap<String, Function.Factory> declared = ArrayListMultimap.create();
    static
    {
        // All method sharing the same name must have the same returnType. We could find a way to make that clear.
        declared.put("token", TokenFct.factory);

        declared.put("now", AbstractFunction.factory(TimeuuidFcts.nowFct));
        declared.put("mintimeuuid", AbstractFunction.factory(TimeuuidFcts.minTimeuuidFct));
        declared.put("maxtimeuuid", AbstractFunction.factory(TimeuuidFcts.maxTimeuuidFct));
        declared.put("dateof", AbstractFunction.factory(TimeuuidFcts.dateOfFct));
        declared.put("unixtimestampof", AbstractFunction.factory(TimeuuidFcts.unixTimestampOfFct));

        for (CQL3Type type : CQL3Type.Native.values())
        {
            // Note: because text and varchar ends up being synonimous, our automatic makeToBlobFunction doesn't work
            // for varchar, so we special case it below. We also skip blob for obvious reasons.
            if (type == CQL3Type.Native.VARCHAR || type == CQL3Type.Native.BLOB)
                continue;

            Function toBlob = BytesConversionFcts.makeToBlobFunction(type.getType());
            Function fromBlob = BytesConversionFcts.makeFromBlobFunction(type.getType());
            declared.put(toBlob.name(), AbstractFunction.factory(toBlob));
            declared.put(fromBlob.name(), AbstractFunction.factory(fromBlob));
        }
        declared.put("varcharasblob", AbstractFunction.factory(BytesConversionFcts.VarcharAsBlobFct));
        declared.put("blobasvarchar", AbstractFunction.factory(BytesConversionFcts.BlobAsVarcharFact));
    }

    public static AbstractType<?> getReturnType(String functionName, String ksName, String cfName)
    {
        List<Function.Factory> factories = declared.get(functionName.toLowerCase());
        return factories.isEmpty()
             ? null // That's ok, we'll complain later
             : factories.get(0).create(ksName, cfName).returnType();
    }

    public static ColumnSpecification makeArgSpec(ColumnSpecification receiver, Function fun, int i)
    {
        return new ColumnSpecification(receiver.ksName,
                receiver.cfName,
                new ColumnIdentifier("arg" + i +  "(" + fun.name() + ")", true),
                fun.argsType().get(i));
    }

    public static Function get(String name, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver) throws InvalidRequestException
    {
        List<Function.Factory> factories = declared.get(name.toLowerCase());
        if (factories.isEmpty())
            throw new InvalidRequestException(String.format("Unknown CQL3 function %s called", name));

        // Fast path if there is not choice
        if (factories.size() == 1)
        {
            Function fun = factories.get(0).create(receiver.ksName, receiver.cfName);
            validateTypes(fun, providedArgs, receiver);
            return fun;
        }

        Function candidate = null;
        for (Function.Factory factory : factories)
        {
            Function toTest = factory.create(receiver.ksName, receiver.cfName);
            if (!isValidType(toTest, providedArgs, receiver))
                continue;

            if (candidate == null)
                candidate = toTest;
            else
                throw new InvalidRequestException(String.format("Ambiguous call to function %s (can match both type signature %s and %s): use type casts to disambiguate", name, signature(candidate), signature(toTest)));
        }
        if (candidate == null)
            throw new InvalidRequestException(String.format("Invalid call to function %s, none of its type signature matches (known type signatures: %s)", name, signatures(factories, receiver)));
        return candidate;
    }

    private static void validateTypes(Function fun, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver) throws InvalidRequestException
    {
        if (!receiver.type.asCQL3Type().equals(fun.returnType().asCQL3Type()))
            throw new InvalidRequestException(String.format("Type error: cannot assign result of function %s (type %s) to %s (type %s)", fun.name(), fun.returnType().asCQL3Type(), receiver, receiver.type.asCQL3Type()));

        if (providedArgs.size() != fun.argsType().size())
            throw new InvalidRequestException(String.format("Invalid number of arguments in call to function %s: %d required but %d provided", fun.name(), fun.argsType().size(), providedArgs.size()));

        for (int i = 0; i < providedArgs.size(); i++)
        {
            AssignementTestable provided = providedArgs.get(i);

            // If the concrete argument is a bind variables, it can have any type.
            // We'll validate the actually provided value at execution time.
            if (provided == null)
                continue;

            ColumnSpecification expected = makeArgSpec(receiver, fun, i);
            if (!provided.isAssignableTo(expected))
                throw new InvalidRequestException(String.format("Type error: %s cannot be passed as argument %d of function %s of type %s", provided, i, fun.name(), expected.type.asCQL3Type()));
        }
    }

    private static boolean isValidType(Function fun, List<? extends AssignementTestable> providedArgs, ColumnSpecification receiver)
    {
        if (!receiver.type.asCQL3Type().equals(fun.returnType().asCQL3Type()))
            return false;

        if (providedArgs.size() != fun.argsType().size())
            return false;

        for (int i = 0; i < providedArgs.size(); i++)
        {
            AssignementTestable provided = providedArgs.get(i);

            // If the concrete argument is a bind variables, it can have any type.
            // We'll validate the actually provided value at execution time.
            if (provided == null)
                continue;

            ColumnSpecification expected = makeArgSpec(receiver, fun, i);
            if (!provided.isAssignableTo(expected))
                return false;
        }
        return true;
    }

    private static String signature(Function fun)
    {
        List<AbstractType<?>> args = fun.argsType();
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        for (int i = 0; i < args.size(); i++)
        {
            if (i > 0) sb.append(", ");
            sb.append(args.get(i).asCQL3Type());
        }
        sb.append(") -> ");
        sb.append(fun.returnType().asCQL3Type());
        return sb.toString();
    }

    private static String signatures(List<Function.Factory> factories, ColumnSpecification receiver)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < factories.size(); i++)
        {
            if (i > 0) sb.append(", ");
            sb.append(signature(factories.get(i).create(receiver.ksName, receiver.cfName)));
        }
        return sb.toString();
    }
}
