// © 2017 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
package com.ibm.icu.dev.test.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import com.ibm.icu.dev.test.AbstractTestLog;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.impl.locale.XCldrStub.FileUtilities;
import com.ibm.icu.impl.locale.XCldrStub.Splitter;
import com.ibm.icu.util.ICUUncheckedIOException;

abstract public class DataDrivenTestHelper {

    public static final List<String> DEBUG_LINE = Collections.singletonList("@debug");
    public static final Splitter SEMICOLON = Splitter.on(';').trimResults();
    public static final Splitter EQUAL_SPLIT = Splitter.on('=').trimResults();
    public static final String SEPARATOR = " ; \t";

    protected TestFmwk framework = null;
    protected int minArgumentCount = 3;
    protected int maxArgumentCount = 4;
    private List<List<String>> lines = new ArrayList<>();
    private List<String> comments = new ArrayList<>();

    public DataDrivenTestHelper setFramework(TestFmwk testFramework) {
        this.framework = testFramework;
        return this;
    }

    public <T extends Appendable> T appendLines(T out) {
        try {
            for (int i = 0; i < lines.size(); ++i) {
                List<String> components = lines.get(i);
                String comment = comments.get(i);
                if (components.isEmpty()) {
                    if(!comment.isEmpty()) {
                        out.append("# ").append(comment);
                    }
                } else {
                    String first = components.iterator().next();
                    String sep = first.startsWith("@") ? "=" : SEPARATOR;
                    out.append(CollectionUtilities.join(components, sep));
                    if (!comment.isEmpty()) {
                        out.append("\t# ").append(comment);
                    }
                }
                out.append('\n');
            }
            return out;
        } catch (IOException e) {
            throw new ICUUncheckedIOException(e);
        }
    }

    protected DataDrivenTestHelper addLine(List<String> arguments, String commentBase) {
        lines.add(Collections.unmodifiableList(arguments));
        comments.add(commentBase);
        return this;
    }

    public DataDrivenTestHelper run(Class<?> classFileIsRelativeTo, String file) {
        return load(classFileIsRelativeTo, file)
            .test();
    }

    public boolean isTestLine(List<String> arguments) {
        return !arguments.isEmpty() && !arguments.equals(DEBUG_LINE);
    }

    public DataDrivenTestHelper test() {
        boolean breakpoint = false;
        for (int i = 0; i < lines.size(); ++i) {
            List<String> arguments = lines.get(i);
            String comment = comments.get(i);
            if (arguments.isEmpty()) {
                if (!comment.isEmpty()) {
                    AbstractTestLog.logln(comment);
                }
                continue;
            } else if (arguments.equals(DEBUG_LINE)) {
                breakpoint = true;
                continue;
            } else {
                String first = arguments.get(0);
                if (first.startsWith("@")) {
                    handleParams(comment, arguments);
                    continue;
                }
            }
            try {
                handle(i, breakpoint, comment, arguments);
            } catch (Exception e) {
                e.printStackTrace();
                AbstractTestLog.errln("Illegal data test file entry (" + i + "): " + arguments + " # " + comment);
            }
            breakpoint = false;
        }
        return this;
    }

    public DataDrivenTestHelper load(Class<?> classFileIsRelativeTo, String file) {
        BufferedReader in = null;
        try {
            in = FileUtilities.openFile(classFileIsRelativeTo, file);
            //boolean breakpoint = false;

            while (true) {
                String line = in.readLine();
                if (line == null) {
                    break;
                }
                line = line.trim();
                if (line.isEmpty()) {
                    addLine(Collections.<String>emptyList(), "");
                    continue;
                }
                int hash = line.indexOf('#');
                String comment = "";
                String commentBase = "";
                if (hash >= 0) {
                    commentBase = line.substring(hash+1).trim();
                    line = line.substring(0,hash).trim();
                    comment = "# " + commentBase;
                    if (!line.isEmpty()) {
                        comment = "\t" + comment;
                    }
                }
                if (line.isEmpty()) {
                    addLine(Collections.<String>emptyList(), commentBase);
                    continue;
                }
                if (line.startsWith("@")) {
                    List<String> keyValue = EQUAL_SPLIT.splitToList(line);
                    addLine(keyValue, comment);
                    continue;
                }
                List<String> arguments = SEMICOLON.splitToList(line);
                if (arguments.size() < minArgumentCount || arguments.size() > maxArgumentCount) {
                    AbstractTestLog.errln("Malformed data line:" + line + comment);
                    continue;
                }
                addLine(arguments, commentBase);
            }
        } catch (IOException e) {
            throw new ICUUncheckedIOException(e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    throw new ICUUncheckedIOException(e);
                }
            }
        }
        lines = Collections.unmodifiableList(lines); // should do deep unmodifiable...
        comments = Collections.unmodifiableList(comments);
        return this;
    }

    protected boolean assertEquals(String message, Object expected, Object actual) {
        return TestFmwk.handleAssert(Objects.equals(expected, actual), message, stringFor(expected), stringFor(actual), null, false);
    }

    private final String stringFor(Object obj) {
        return obj == null ? "null"
            : obj instanceof String ? "\"" + obj + '"'
                : obj instanceof Number ? String.valueOf(obj)
                    : obj.getClass().getName() + "<" + obj + ">";
    }

    abstract public void handle(int lineNumber, boolean breakpoint, String commentBase, List<String> arguments);

    public void handleParams(String comment, List<String> arguments) {
        throw new IllegalArgumentException("Unrecognized parameter: " + arguments);
    }

    public List<List<String>> getLines() {
        return lines;
    }
}
