/*
 * 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.kylin.dimension;

import java.util.Map;

import javax.annotation.Nullable;

import org.apache.kylin.common.KylinConfig;
import org.apache.kylin.common.util.ClassUtil;
import org.apache.kylin.common.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;

public abstract class DimensionEncodingFactory {

    private static final Logger logger = LoggerFactory.getLogger(DimensionEncodingFactory.class);

    private static Map<Pair<String, Integer>, DimensionEncodingFactory> factoryMap;

    /**
     * If a bug found in a DimEnc will cause different cube outputs,
     * we'll have to increase the version number of DimEnc, in order
     * to distinguish current version with prior version.
     * <p>
     * The default version applys to all existing legacy DimEncs
     */
    protected int getCurrentVersion() {
        return 1;
    }

    /**
     * Create a DimensionEncoding instance, with inputs corresponding to RowKeyColDesc.encodingName and RowKeyColDesc.encodingArgs.
     */
    public static DimensionEncoding create(String encodingName, String[] args, int version) {
        if (factoryMap == null)
            initFactoryMap();

        DimensionEncodingFactory factory = factoryMap.get(Pair.newPair(encodingName, version));
        if (factory == null) {
            throw new IllegalArgumentException("Unknown dimension encoding name " + encodingName //
                    + " (note '" + DictionaryDimEnc.ENCODING_NAME + "' is not handled by factory)");
        }

        return factory.createDimensionEncoding(encodingName, args);
    }

    public static Map<String, Integer> getValidEncodings() {
        if (factoryMap == null)
            initFactoryMap();

        Map<String, Integer> result = Maps.newTreeMap();
        for (Pair<String, Integer> p : factoryMap.keySet()) {
            if (result.containsKey(p.getFirst())) {
                if (result.get(p.getFirst()) > p.getSecond()) {
                    continue;//skip small versions
                }
            }

            result.put(p.getFirst(), p.getSecond());
        }
        result.put(DictionaryDimEnc.ENCODING_NAME, 1);
        return result;
    }

    public static boolean isValidEncoding(final String encodingName) {
        if (factoryMap == null)
            initFactoryMap();

        // note dictionary is a special case
        return DictionaryDimEnc.ENCODING_NAME.equals(encodingName) || //
                Iterables.any(factoryMap.keySet(), new Predicate<Pair<String, Integer>>() {
                    @Override
                    public boolean apply(@Nullable Pair<String, Integer> input) {
                        return input.getFirst().equals(encodingName);
                    }
                });
    }

    public static boolean isValidEncoding(final String encodingName, String[] args, int version) {
        if (factoryMap == null)
            initFactoryMap();

        DimensionEncodingFactory factory = factoryMap.get(Pair.newPair(encodingName, version));
        if (factory == null) {
            return true;
        }

        return factory.isValidDimensionEncoding(encodingName, args);
    }

    private static synchronized void initFactoryMap() {
        if (factoryMap == null) {
            Map<Pair<String, Integer>, DimensionEncodingFactory> map = Maps.newConcurrentMap();

            // built-in encodings, note dictionary is a special case
            {
                FixedLenDimEnc.Factory value = new FixedLenDimEnc.Factory();
                map.put(Pair.newPair(FixedLenDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                IntDimEnc.Factory value = new IntDimEnc.Factory();
                map.put(Pair.newPair(IntDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                IntegerDimEnc.Factory value = new IntegerDimEnc.Factory();
                map.put(Pair.newPair(IntegerDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                FixedLenHexDimEnc.Factory value = new FixedLenHexDimEnc.Factory();
                map.put(Pair.newPair(FixedLenHexDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                DateDimEnc.Factory value = new DateDimEnc.Factory();
                map.put(Pair.newPair(DateDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                TimeDimEnc.Factory value = new TimeDimEnc.Factory();
                map.put(Pair.newPair(TimeDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }
            {
                BooleanDimEnc.Factory value = new BooleanDimEnc.Factory();
                map.put(Pair.newPair(BooleanDimEnc.ENCODING_NAME, value.getCurrentVersion()), value);
            }

            // custom encodings
            String[] clsNames = KylinConfig.getInstanceFromEnv().getCubeDimensionCustomEncodingFactories();
            for (String clsName : clsNames) {
                try {
                    DimensionEncodingFactory factory = (DimensionEncodingFactory) ClassUtil.newInstance(clsName);
                    map.put(Pair.newPair(factory.getSupportedEncodingName(), factory.getCurrentVersion()), factory);
                } catch (Exception ex) {
                    logger.error("Failed to init dimension encoding factory " + clsName, ex);
                }
            }

            factoryMap = map;
        }
    }

    /**
     * Return the supported encoding name, corresponds to RowKeyColDesc.encodingName
     */
    abstract public String getSupportedEncodingName();

    /**
     * Create a DimensionEncoding instance, with inputs corresponding to RowKeyColDesc.encodingName and RowKeyColDesc.encodingArgs
     */
    abstract public DimensionEncoding createDimensionEncoding(String encodingName, String[] args);

    /**
     * Validate the inputs of RowKeyColDesc.encodingName and RowKeyColDesc.encodingArgs for a DimensionEncoding instance
     */
    abstract public boolean isValidDimensionEncoding(String encodingName, String[] args);
}
