/*
 * Copyright 2017 HugeGraph Authors
 *
 * 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 com.baidu.hugegraph.auth;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.tinkerpop.gremlin.structure.Graph.Hidden;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.shaded.jackson.annotation.JsonProperty;
import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator;
import org.apache.tinkerpop.shaded.jackson.core.JsonParser;
import org.apache.tinkerpop.shaded.jackson.core.JsonToken;
import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference;
import org.apache.tinkerpop.shaded.jackson.databind.DeserializationContext;
import org.apache.tinkerpop.shaded.jackson.databind.SerializerProvider;
import org.apache.tinkerpop.shaded.jackson.databind.deser.std.StdDeserializer;
import org.apache.tinkerpop.shaded.jackson.databind.module.SimpleModule;
import org.apache.tinkerpop.shaded.jackson.databind.ser.std.StdSerializer;

import com.baidu.hugegraph.HugeException;
import com.baidu.hugegraph.auth.ResourceType;
import com.baidu.hugegraph.auth.SchemaDefine.UserElement;
import com.baidu.hugegraph.structure.HugeElement;
import com.baidu.hugegraph.traversal.optimize.TraversalUtil;
import com.baidu.hugegraph.type.Namifiable;
import com.baidu.hugegraph.type.Typifiable;
import com.baidu.hugegraph.util.JsonUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

public class HugeResource {

    public static final String ANY = "*";

    public static final HugeResource ALL = new HugeResource(ResourceType.ALL,
                                                            ANY, null);
    public static final List<HugeResource> ALL_RES = ImmutableList.of(ALL);

    private static final Set<ResourceType> CHECK_NAME_RESS = ImmutableSet.of(
                                                             ResourceType.META);

    static {
        SimpleModule module = new SimpleModule();

        module.addSerializer(HugeResource.class, new HugeResourceSer());
        module.addDeserializer(HugeResource.class, new HugeResourceDeser());

        JsonUtil.registerModule(module);
    }

    @JsonProperty("type")
    private ResourceType type = ResourceType.NONE;

    @JsonProperty("label")
    private String label = ANY;

    @JsonProperty("properties")
    private Map<String, Object> properties; // value can be predicate

    public HugeResource() {
        // pass
    }

    public HugeResource(ResourceType type, String label,
                        Map<String, Object> properties) {
        this.type = type;
        this.label = label;
        this.properties = properties;
        this.checkFormat();
    }

    public void checkFormat() {
        if (this.properties == null) {
            return;
        }
        for (Map.Entry<String, Object> entry : this.properties.entrySet()) {
            String propName = entry.getKey();
            Object propValue = entry.getValue();
            if (propName.equals(ANY) && propValue.equals(ANY)) {
                continue;
            }
            if (propValue instanceof String &&
                ((String) propValue).startsWith(TraversalUtil.P_CALL)) {
                TraversalUtil.parsePredicate((String) propValue);
            }
        }
    }

    public boolean filter(ResourceObject<?> resourceObject) {
        if (this.type == null || this.type == ResourceType.NONE) {
            return false;
        }

        if (!this.type.match(resourceObject.type())) {
            return false;
        }

        if (resourceObject.operated() != NameObject.ANY) {
            ResourceType resType = resourceObject.type();
            if (resType.isGraph()) {
                return this.filter((HugeElement) resourceObject.operated());
            }
            if (resType.isUsers()) {
                return this.filter((UserElement) resourceObject.operated());
            }
            if (resType.isSchema() || CHECK_NAME_RESS.contains(resType)) {
                return this.filter((Namifiable) resourceObject.operated());
            }
        }

        /*
         * Allow any others resource if the type is matched:
         * VAR, GREMLIN, GREMLIN_JOB, TASK
         */
        return true;
    }

    private boolean filter(UserElement element) {
        assert this.type.match(element.type());
        if (element instanceof Namifiable) {
            if (!this.filter((Namifiable) element)) {
                return false;
            }
        }
        return true;
    }

    private boolean filter(Namifiable element) {
        assert !(element instanceof Typifiable) || this.type.match(
               ResourceType.from(((Typifiable) element).type()));

        if (!this.matchLabel(element.name())) {
            return false;
        }
        return true;
    }

    private boolean filter(HugeElement element) {
        assert this.type.match(ResourceType.from(element.type()));

        if (!this.matchLabel(element.label())) {
            return false;
        }

        if (this.properties == null) {
            return true;
        }
        for (Map.Entry<String, Object> entry : this.properties.entrySet()) {
            String propName = entry.getKey();
            Object expected = entry.getValue();
            if (propName.equals(ANY) && expected.equals(ANY)) {
                return true;
            }
            Property<Object> prop = element.property(propName);
            if (!prop.isPresent()) {
                return false;
            }
            try {
                if (!TraversalUtil.testProperty(prop, expected)) {
                    return false;
                }
            } catch (IllegalArgumentException e) {
                throw new HugeException("Invalid resouce '%s' for '%s': %s",
                                        expected, propName, e.getMessage());
            }
        }
        return true;
    }

    private boolean matchLabel(String other) {
        // Label value may be vertex/edge label or schema name
        if (this.label == null || other == null) {
            return false;
        }
        if (!this.label.equals(ANY) && !other.matches(this.label)) {
            return false;
        }
        return true;
    }

    private boolean matchProperties(Map<String, Object> other) {
        if (this.properties == null) {
            // Any property is OK
            return true;
        }
        if (other == null) {
            return false;
        }
        for (Map.Entry<String, Object> p : other.entrySet()) {
            Object value = this.properties.get(p.getKey());
            if (!Objects.equals(value, p.getValue())) {
                return false;
            }
        }
        return true;
    }

    protected boolean contains(HugeResource other) {
        if (this.equals(other)) {
            return true;
        }
        if (this.type == null || this.type == ResourceType.NONE) {
            return false;
        }
        if (!this.type.match(other.type)) {
            return false;
        }
        if (!this.matchLabel(other.label)) {
            return false;
        }
        if (!this.matchProperties(other.properties)) {
            return false;
        }
        return true;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof HugeResource)) {
            return false;
        }
        HugeResource other = (HugeResource) object;
        return this.type == other.type &&
               Objects.equals(this.label, other.label) &&
               Objects.equals(this.properties, other.properties);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.type, this.label, this.properties);
    }

    @Override
    public String toString() {
        return JsonUtil.toJson(this);
    }

    public static boolean allowed(ResourceObject<?> resourceObject) {
        // Allowed to access system(hidden) schema by anyone
        if (resourceObject.type().isSchema()) {
            Namifiable schema = (Namifiable) resourceObject.operated();
            if (Hidden.isHidden(schema.name())) {
                return true;
            }
        }

        return false;
    }

    public static HugeResource parseResource(String resource) {
        return JsonUtil.fromJson(resource, HugeResource.class);
    }

    public static List<HugeResource> parseResources(String resources) {
        TypeReference<?> type = new TypeReference<List<HugeResource>>() {};
        return JsonUtil.fromJson(resources, type);
    }

    public static class NameObject implements Namifiable {

        public static final NameObject ANY = new NameObject("*");

        private final String name;

        public static NameObject of(String name) {
            return new NameObject(name);
        }

        private NameObject(String name) {
            this.name = name;
        }

        @Override
        public String name() {
            return this.name;
        }

        @Override
        public String toString() {
            return this.name;
        }
    }

    private static class HugeResourceSer extends StdSerializer<HugeResource> {

        private static final long serialVersionUID = -138482122210181714L;

        public HugeResourceSer() {
            super(HugeResource.class);
        }

        @Override
        public void serialize(HugeResource res, JsonGenerator generator,
                              SerializerProvider provider)
                              throws IOException {
            generator.writeStartObject();

            generator.writeObjectField("type", res.type);
            generator.writeObjectField("label", res.label);
            generator.writeObjectField("properties", res.properties);

            generator.writeEndObject();
        }
    }

    private static class HugeResourceDeser extends StdDeserializer<HugeResource> {

        private static final long serialVersionUID = -2499038590503066483L;

        public HugeResourceDeser() {
            super(HugeResource.class);
        }

        @Override
        public HugeResource deserialize(JsonParser parser,
                                        DeserializationContext ctxt)
                                        throws IOException {
            HugeResource res = new HugeResource();
            while (parser.nextToken() != JsonToken.END_OBJECT) {
                String key = parser.getCurrentName();
                if (key.equals("type")) {
                    if (parser.nextToken() != JsonToken.VALUE_NULL) {
                        res.type = ctxt.readValue(parser, ResourceType.class);
                    } else {
                        res.type = null;
                    }
                } else if (key.equals("label")) {
                    if (parser.nextToken() != JsonToken.VALUE_NULL) {
                        res.label = parser.getValueAsString();
                    } else {
                        res.label = null;
                    }
                } else if (key.equals("properties")) {
                    if (parser.nextToken() != JsonToken.VALUE_NULL) {
                        @SuppressWarnings("unchecked")
                        Map<String, Object> prop = ctxt.readValue(parser,
                                                                  Map.class);
                        res.properties = prop;
                    } else {
                        res.properties = null;
                    }
                }
            }
            res.checkFormat();
            return res;
        }
    }
}