/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.client.admin.cli.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * @author <a href="mailto:marko.strukelj@gmail.com">Marko Strukelj</a>
 */
public class ReturnFields implements Iterable<String> {

    public static ReturnFields ALL = new ReturnFields() {
        @Override
        public ReturnFields child(String field) {
            return NONE;
        }

        @Override
        public boolean included(String... pathSegments) {
            return true;
        }

        @Override
        public boolean excluded(String field) {
            return false;
        }

        @Override
        public Iterator<String> iterator() {
            return Collections.singletonList("*").iterator();
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        public boolean isAll() {
            return true;
        }

        @Override
        public String toString() {
            return "[ReturnFields ALL]";
        }
    };

    public static ReturnFields NONE = new ReturnFields() {
        @Override
        public ReturnFields child(String field) {
            return this;
        }

        @Override
        public boolean included(String... pathSegments) {
            return false;
        }

        @Override
        public boolean excluded(String field) {
            return false;
        }

        @Override
        public Iterator<String> iterator() {
            List<String> emptyList = Collections.emptyList();
            return emptyList.iterator();
        }

        @Override
        public boolean isEmpty() {
            return true;
        }

        @Override
        public boolean isAll() {
            return false;
        }

        @Override
        public String toString() {
            return "[ReturnFields NONE]";
        }
    };

    public static ReturnFields ALL_RECURSIVELY = new ReturnFields() {
        @Override
        public ReturnFields child(String field) {
            return this;
        }

        @Override
        public boolean included(String... pathSegments) {
            return true;
        }

        @Override
        public boolean excluded(String field) {
            return false;
        }

        @Override
        public Iterator<String> iterator() {
            List<String> emptyList = Collections.emptyList();
            return emptyList.iterator();
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public boolean isAll() {
            return true;
        }
    };
    
    private enum TargetState {
        IdentCommaOpen,
        Ident,
        Comma,
        Anything
    }

    private enum FieldState {
        start,
        name,
        end
    }


    private HashMap<String, ReturnFields> fields = new LinkedHashMap<>();
    
    
    
    public ReturnFields() {}
    
    public ReturnFields(String spec) {

        if (spec == null || spec.trim().length() == 0) {
            throw new IllegalArgumentException("Fields spec is null or empty!");
        }
        // parse the spec, building up the tree for nested children
        char[] buf = spec.toCharArray();
        StringBuilder token = new StringBuilder(buf.length);

        // stack for handling depth
        LinkedList<HashMap<String, ReturnFields>> specs = new LinkedList<>();
        specs.add(fields);

        // parser state
        FieldState fldState = FieldState.start;
        TargetState state = TargetState.Ident;

        int i;
        for (i = 0; i < buf.length; i++) {
            char c = buf[i];

            if (c == ',') {
                if (state == TargetState.Ident) {
                    error(spec, i);
                }
                if (fldState == FieldState.name) {
                    specs.getLast().put(token.toString(), null);
                    token.setLength(0);
                }
                state = TargetState.Ident;
                fldState = FieldState.start;
            } else if (c == '(') {
                if (state != TargetState.IdentCommaOpen && state != TargetState.Anything) {
                    error(spec, i);
                }
                ReturnFields sub = new ReturnFields();
                specs.getLast().put(token.toString(), sub);
                specs.add(sub.fields);
                token.setLength(0);

                state = TargetState.Ident;
                fldState = FieldState.start;
            } else if (c == ')') {
                if (state != TargetState.Anything) {
                    error(spec, i);
                }
                if (fldState == FieldState.name) {
                    specs.getLast().put(token.toString(), null);
                    token.setLength(0);

                }
                specs.removeLast();

                fldState = FieldState.end;
                state = specs.size() > 1 ? TargetState.Anything : TargetState.Comma;
            } else {
                token.append(c);
                if (fldState == FieldState.start) {
                    fldState = FieldState.name;
                    state = specs.size() > 1 ? TargetState.Anything : TargetState.IdentCommaOpen;
                }
            }
        }

        if (specs.size() > 1) {
            error(spec, i);
        }

        if (token.length() > 0) {
            specs.getLast().put(token.toString(), null);
        } else if (!(state == TargetState.Anything || state == TargetState.Comma)) {
            error(spec, i);
        }
    }

    private void error(String spec, int i) {
        throw new RuntimeException("Invalid fields specification at position " + i + ": " + spec);
    }

    
    
    /**
     * Get ReturnFields for a child field of JSONObject type.
     *
     * <p>For basic-typed fields this always returns null. Use included() for those.</p>
     *
     * @param field The child field name for nested returns.
     * @return ReturnFields for a child field
     */
    public ReturnFields child(String field) {
        ReturnFields returnFields = fields.get(field);
        if (returnFields == null) {
            returnFields = fields.get("*");
            if (returnFields == null) {
                returnFields = ReturnFields.NONE;
            }
        }
        return returnFields;
    }

    /**
     * Check to see if the field should be included in JSON response.
     *
     * <p>The check can be performed for any level of depth relative to current nesting level, by specifying multiple path segments.</p>
     *
     * @param pathSegments Segments to test in the tree of return fields.
     * @return true if the specified path should be part of JSON response or not
     */
    public boolean included(String... pathSegments) {

        if (pathSegments == null || pathSegments.length == 0) {
            throw new IllegalArgumentException("No path specified!");
        }
        ReturnFields current = this;

        for (String path : pathSegments) {
            if (current == null) {
                return false;
            }

            if (current.fields.containsKey("-" + path)) {
                return false;
            }
            if (current.fields.containsKey("*")) {
                return true;
            }
            if (!current.fields.containsKey(path)) {
                return false;
            }
            current = current.fields.get(path);
        }
        return true;
    }

    /**
     * Check to see if the field specified is set to be explicitly excluded.
     * @param field The field name to check
     * @return If the field was explicitly set to be excluded
     */
    public boolean excluded(String field) {
        if (fields.containsKey("-" + field)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Iterate over child fields to be included in response.
     *
     * <p>To get nested field specifier use child(name) passing the field name this iterator returns.</p>
     *
     * @return iterator over child fields to be included in response.
     */
    public Iterator<String> iterator() {
        return fields.keySet().iterator();
    }

    /**
     * Determine if zero fields should be returned.
     *
     * @return <code>true</code> if the list is empty, else, <code>false</code>
     */
    public boolean isEmpty() {
        return this.fields.isEmpty();
    }

    public boolean isAll() {
        return this.fields.keySet().contains("*");
    }

    @Override
    public String toString() {
        return "[ReturnFieldsImpl: fields=" + this.fields + "]";
    }
}
