/*
 * 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.curator.framework.schema;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.apache.curator.framework.CuratorFramework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Collection of all schemas for a Curator instance
 */
public class SchemaSet {
    private final Logger log = LoggerFactory.getLogger(getClass());
    private final Map<String, Schema> schemas;
    private final Map<String, Schema> pathToSchemas;
    private final List<Schema> regexSchemas;
    private final CacheLoader<String, Schema> cacheLoader = new CacheLoader<String, Schema>() {
        @Override
        public Schema load(String path) throws Exception {
            for (Schema schema : regexSchemas) {
                if (schema.getPathRegex().matcher(path).matches()) {
                    log.debug("path -> {}", schema);
                    return schema;
                }
            }
            return nullSchema;
        }
    };
    private final LoadingCache<String, Schema> regexCache =
            CacheBuilder.newBuilder().softValues().build(cacheLoader);

    private static final Schema nullSchema = new Schema(
            "__null__",
            null,
            "",
            "Null schema",
            new DefaultSchemaValidator(),
            Schema.Allowance.CAN,
            Schema.Allowance.CAN,
            Schema.Allowance.CAN,
            true,
            ImmutableMap.<String, String>of());
    private static final Schema defaultSchema = new Schema(
            "__default__",
            null,
            "",
            "Default schema",
            new DefaultSchemaValidator(),
            Schema.Allowance.CAN,
            Schema.Allowance.CAN,
            Schema.Allowance.CAN,
            true,
            ImmutableMap.<String, String>of());
    private final boolean useDefaultSchema;

    /**
     * Return the default (empty) schema set
     *
     * @return default schema set
     */
    public static SchemaSet getDefaultSchemaSet() {
        return new SchemaSet(Collections.<Schema>emptyList(), true) {
            @Override
            public String toDocumentation() {
                return "Default schema";
            }
        };
    }

    /**
     * Define a schema set. Schemas are matched in a well defined order:
     * <ol>
     *     <li>Exact match on full path (i.e. non-regex)</li>
     *     <li>Match on the first regex path, searched in the order given to this constructor</li>
     * </ol>
     *
     * @param schemas the schemas for the set.
     * @param useDefaultSchema if true, return a default schema when there is no match. Otherwise, an exception is thrown
     */
    public SchemaSet(List<Schema> schemas, boolean useDefaultSchema) {
        schemas = Preconditions.checkNotNull(schemas, "schemas cannot be null");

        this.useDefaultSchema = useDefaultSchema;
        this.schemas = Maps.uniqueIndex(schemas, new Function<Schema, String>() {
            @Override
            public String apply(Schema schema) {
                return schema.getName();
            }
        });
        ImmutableMap.Builder<String, Schema> pathBuilder = ImmutableMap.builder();
        ImmutableList.Builder<Schema> regexBuilder = ImmutableList.builder();
        for (Schema schema : schemas) {
            if (schema.getPath() != null) {
                pathBuilder.put(schema.getPath(), schema);
            } else {
                regexBuilder.add(schema);
            }
        }
        pathToSchemas = pathBuilder.build();
        regexSchemas = regexBuilder.build();
    }

    /**
     * Return the schemas
     *
     * @return schemas
     */
    public Collection<Schema> getSchemas() {
        return schemas.values();
    }

    /**
     * Find the first matching schema for the path and return it
     *
     * @param path ZNode full path
     * @return matching schema or a default schema
     */
    public Schema getSchema(String path) {
        if (schemas.size() > 0) {
            Schema schema = pathToSchemas.get(path);
            if (schema == null) {
                try {
                    schema = regexCache.get(path);
                    if (schema.equals(nullSchema)) {
                        schema = useDefaultSchema ? defaultSchema : null;
                    }
                } catch (ExecutionException e) {
                    throw new RuntimeException(e);
                }
            }
            if (schema != null) {
                return schema;
            }
        }
        if (useDefaultSchema) {
            return defaultSchema;
        }
        throw new SchemaViolation(
                null, new SchemaViolation.ViolatorData(path, null, null), "No schema found for: " + path);
    }

    /**
     * Utility to return a ZNode path for the given name
     *
     * @param client Curator client
     * @param name path/schema name
     * @return ZNode path
     */
    public static String getNamedPath(CuratorFramework client, String name) {
        return client.getSchemaSet().getNamedSchema(name).getRawPath();
    }

    /**
     * Return the schema with the given key/name
     *
     * @param name name
     * @return schema or null
     */
    public Schema getNamedSchema(String name) {
        return schemas.get(name);
    }

    /**
     * Build a user displayable documentation string for the schemas in this set
     *
     * @return documentation
     */
    public String toDocumentation() {
        StringBuilder str = new StringBuilder("Curator Schemas:\n\n");
        for (Map.Entry<String, Schema> schemaEntry : schemas.entrySet()) {
            str.append(schemaEntry.getKey())
                    .append('\n')
                    .append(schemaEntry.getValue().toDocumentation())
                    .append('\n');
        }
        return str.toString();
    }
}
