/*
 * Copyright (c) nosqlbench
 *
 * 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 io.nosqlbench.virtdata.core.templates;

import io.nosqlbench.nb.api.errors.BasicError;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.regex.Matcher.quoteReplacement;

/**
 * BindPointParser parses a user-provide string template into spans. It builds a simple list of
 * BindPoints, and provides both the parsed spans and the BindPoints in a result.
 * Spans are provided as a list of even,(odd,even,...) string values, where there is always an odd number of entries
 * and odd-numbered entries are the text extracted from a binding. The binding values are not
 * changed from the user-specified string, and are not normalized to one of the {@link BindPoint.Type}s.
 * This is for troubleshooting and diagnostic purposes.
 * Callers will need to look at the BindPoint types to know whether a binding is provided and thus how to process it.
 */
public class BindPointParser implements BiFunction<String, Map<String, String>, BindPointParser.Result> {

    public final static Pattern BINDPOINT_ANCHOR = Pattern.compile(
        "(\\{((?<reference>\\w+[-_<>,\\d\\w.]*)})|(\\{\\{(?<inline1>(?!}}).+?)}})|(\\{\\((?<inline2>(?!\\)}).+?)\\)}))"
    );
    public final static String DEFINITION = "DEFINITION";


    @Override
    public Result apply(String template, Map<String, String> bindings) {

        Matcher m = BINDPOINT_ANCHOR.matcher(template);
        int lastMatch = 0;
        List<String> spans = new ArrayList<>();
        List<BindPoint> bindpoints = new ArrayList<>();

        int genid = 0;
        while (m.find()) {
            String pre = template.substring(lastMatch, m.start());
            spans.add(unescape(pre));
            lastMatch = m.end();

            String reference = m.group("reference");
            String inline1 = m.group("inline1");
            String inline2 = m.group("inline2");
            if (reference != null) {
                bindpoints.add(BindPoint.of(reference, bindings.getOrDefault(reference, null), BindPoint.Type.reference));
                spans.add(reference);
            } else if (inline1 != null) {
                bindpoints.add(BindPoint.of(DEFINITION, unescape(inline1), BindPoint.Type.definition));
                spans.add(inline1);
            } else if (inline2 != null) {
                bindpoints.add(BindPoint.of(DEFINITION, unescape(inline2), BindPoint.Type.definition));
                spans.add(inline2);
            } else {
                throw new BasicError("Unable to parse: " + template);
            }

        }
        spans.add(lastMatch >= 0 ? unescape(template.substring(lastMatch)) : unescape(template));

        for (String span : spans) {

        }
        return new Result(spans, bindpoints);
    }

    private static String unescape(String s) {
        String s1 = s.replaceAll("\\\\n", "\n");
        return s1;
    }

    public final static class Result {

        private final List<String> spans;
        private final List<BindPoint> bindpoints;

        public Result(List<String> spans, List<BindPoint> bindpoints) {

            this.spans = spans;
            this.bindpoints = bindpoints;
        }

        /**
         * @return the spans of literal values which are between the bind points
         */
        public List<String> getSpans() {
            return spans;
        }

        public List<BindPoint> getBindpoints() {
            return bindpoints;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Result result = (Result) o;
            return Objects.equals(spans, result.spans) && Objects.equals(bindpoints, result.bindpoints);
        }

        @Override
        public int hashCode() {
            return Objects.hash(spans, bindpoints);
        }

        @Override
        public String toString() {
            return "Result{" +
                "spans=" + spans +
                ", bindpoints=" + bindpoints +
                '}';
        }
    }

}
