package be.iminds.ilabt.jfed.testing.base;

import be.iminds.ilabt.jfed.call_log_output.LogOutput;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsFactory;
import be.iminds.ilabt.jfed.log.ApiCallDetails;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.log.ResultListener;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.XMLRPCCallDetails;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * RunTests
 */
public class AutomatedTestRunner {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AutomatedTestRunner.class);

    private final Logger logger;
    private final TargetAuthority testedAuthority;
    private final GeniUser user;
    private final SerializableApiCallDetailsFactory serializableApiCallDetailsFactory;

    @Inject
    public AutomatedTestRunner(Logger logger, TargetAuthority testedAuthority, GeniUser user,
                               SerializableApiCallDetailsFactory serializableApiCallDetailsFactory) {
        this.logger = logger;
        this.testedAuthority = testedAuthority;
        this.user = user;
        this.serializableApiCallDetailsFactory = serializableApiCallDetailsFactory;
    }

    public interface TestListener {
        void onStart(String testname, int testNr, int testCount);
        void onResult(ApiTestResult.ApiTestMethodResult result, int testNr, int testCount);
        void onAllTestDone(ApiTestResult result, int testCount);
    }

    static class SkipTestException extends RuntimeException {
        SkipTestException(String reason) {
            super(reason);
        }
        SkipTestException(String reason, Throwable cause) {
            super(reason, cause);
        }
    }

    private static class DepNode {
        public List<DepNode> dependsOn = new ArrayList<>();
        public List<DepNode> dependers = new ArrayList<>();
        public List<String> groups;
        public Method m;
        public ApiTest.Test a;
        public String mName;
        public DepNode(Method m, ApiTest.Test a) {
            this.m = m;
            this.a = a;
            this.mName = m.getName();
            this.groups = new ArrayList<>();
            Collections.addAll(groups, a.groups());
        }
    }
    private static DepNode findDepNode(String methodName, Collection<DepNode> depNodes) {
        //TODO make more efficient (though I doubt this is an issue)
        for (DepNode d : depNodes) {
            if (Objects.equals(d.m.getName(), methodName)) return d;
        }
        return null;
    }

    private interface DependancyRetriever {
        List<DepNode> getDependancies(DepNode node, List<DepNode> allDepNodes);
    }
    private static class DepNodeSortResult {
        List<List<Method>> methods = new ArrayList<>();
        boolean hasCycle = false;
    }
    private static DepNodeSortResult sortUsingDependancies(Map<Method, ApiTest.Test> methods, DependancyRetriever dependancyRetriever) {
        List<DepNode> unlinkedNodes = new ArrayList<>();

        //   make all depnodes
        for (Map.Entry<Method, ApiTest.Test> e : methods.entrySet()) {
            Method m = e.getKey();
            ApiTest.Test a = e.getValue();

            unlinkedNodes.add(new DepNode(m, a));
        }

        return sortDepNodesUsingDependancies(unlinkedNodes, dependancyRetriever);
    }
    private static DepNodeSortResult sortUsingDependancies(List<Method> methods, DependancyRetriever dependancyRetriever) {
        List<DepNode> unlinkedNodes = new ArrayList<>();

        //   make all depnodes
        for (Method m : methods) {
            Annotation[] annotations = m.getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation instanceof ApiTest.Test) {
                    unlinkedNodes.add(new DepNode(m, (ApiTest.Test)annotation));
                }
            }
        }

        return sortDepNodesUsingDependancies(unlinkedNodes, dependancyRetriever);
    }
    private static DepNodeSortResult sortDepNodesUsingDependancies(List<DepNode> unlinkedNodes, DependancyRetriever dependancyRetriever) {
        //   link all depnodes
        List<DepNode> depNodes = new ArrayList<>(unlinkedNodes);
        for (DepNode d : unlinkedNodes) {
            List<DepNode> deps = dependancyRetriever.getDependancies(d, depNodes);

            for (DepNode other : deps) {
                other.dependers.add(d);
                d.dependsOn.add(other);
            }
        }

        DepNodeSortResult res = new DepNodeSortResult();

        //   extract all depnodes (this step breaks the links and empties the depNodes list!)
        int prevSize = depNodes.size() + 1;
        while (depNodes.size() > 0 && depNodes.size() != prevSize) {
            prevSize = depNodes.size();
            List<DepNode> toRemove = depNodes.stream().filter(d -> d.dependsOn.isEmpty()).collect(Collectors.toList());

            List<Method> depMethList = new ArrayList<>();
            for (DepNode d: toRemove) {
                for (DepNode depender : d.dependers)
                    depender.dependsOn.remove(d);
                depMethList.add(d.m);
            }
            res.methods.add(depMethList);

            depNodes.removeAll(toRemove);
        }
        if (depNodes.size() > 0) {
            res.hasCycle = true;
            List<Method> leftoverList = new ArrayList<>();
            for (DepNode leftover: depNodes) {
                for (DepNode depender : leftover.dependers)
                    depender.dependsOn.remove(leftover);
                leftoverList.add(leftover.m);
            }
            res.methods.add(leftoverList);
        } else
            res.hasCycle = false;

        return res;
    }

    /* keep only methods in group and dependencies */
    private static Map<Method, ApiTest.Test> filterUsingGroup(Map<Method, ApiTest.Test> methods, DependancyRetriever dependancyRetriever, String group) {
        List<DepNode> unlinkedNodes = new ArrayList<>();

        //   make all depnodes
        for (Map.Entry<Method, ApiTest.Test> e : methods.entrySet()) {
            Method m = e.getKey();
            ApiTest.Test a = e.getValue();

            unlinkedNodes.add(new DepNode(m, a));
        }

        //   link all depnodes
        List<DepNode> allDepNodes = new ArrayList<>(unlinkedNodes);
        for (DepNode d : unlinkedNodes) {
            List<DepNode> deps = dependancyRetriever.getDependancies(d, allDepNodes);

            for (DepNode other : deps) {
                other.dependers.add(d);
                d.dependsOn.add(other);
            }
        }

        List<DepNode> depNodes = new ArrayList<>();
        if (group != null) {
            //create new depNodes, which includes only methods in group and dependencies, no others
            List<DepNode> toAdd =
                    allDepNodes.stream().filter(d -> d.groups.contains(group)).collect(Collectors.toList());
            depNodes.addAll(toAdd);
//            System.out.println("Group filter was used. Methods to run (without deps): "+toAdd.size());
            if (toAdd.isEmpty()) {
                throw new RuntimeException("Error: group \""+group+"\" not found. No tests to run.");
            }
            while (!toAdd.isEmpty()) {
                List<DepNode> adding = new ArrayList<>(toAdd);
                toAdd.clear();
                for (DepNode d : adding) {
                    for (DepNode dd : d.dependsOn)
                        if (!depNodes.contains(dd) && !toAdd.contains(dd)) {
                            depNodes.add(dd);
                            toAdd.add(dd);
                        }
                }
            }
//            System.out.println("Group filter was used. Methods to run after adding deps: "+depNodes.size());
        } else {
            depNodes = allDepNodes;
        }

        Map<Method, ApiTest.Test> result = new HashMap<>();
        for (DepNode d : depNodes) {
            result.put(d.m, d.a);
        }
        return result;
    }

    /**
     * @param group if not null, only experiments in this group (and their soft and hard dependencies) will run. */
    public ApiTestResult runTest(Properties testConfig, ApiTest test, String group, TestListener listener, boolean fake) {
//        ApiTest test = null;
//
//        try {
//            test = apiTestFactory
//        } catch (InstantiationException e) {
//            throw new RuntimeException("Exception while instantiating test class "+testClass.getName()+": "+e.getMessage(), e);
//        } catch (IllegalAccessException e) {
//            throw new RuntimeException("Exception while instantiating test class "+testClass.getName()+": "+e.getMessage(), e);
//        }
//        test.setLogger(lo);
        test.setTestConfig(testConfig);

        //TODO: check for required config keys?
//        test.getRequiredConfigKeys();
//        test.getOptionalConfigKeys();

        //get list of annotated methods
        Map<Method, ApiTest.Test> testMethods = new HashMap<>();
        List<Method> methods = Arrays.asList(test.getClass().getMethods());
        for (Method m : methods) {
            Annotation[] annotations = m.getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation instanceof ApiTest.Test) {
                    testMethods.put(m, (ApiTest.Test) annotation);
                }
            }
        }

        List<String> allMethodNames = methods.stream().map(Method::getName).collect(Collectors.toList());

        DependancyRetriever bothDependancyRetriever = new DependancyRetriever() {
            /*allowedMissingMethodnames is used to allow soft deps on methodnames that have been removed due to "group".
            * This whole mecanism may be removed if faulty methodnames are not considered an error. (but it is better to warn for them) */
            List<String> allowedMissingMethodnames;
            public DependancyRetriever init(List<String> allowedMissingMethodnames) {
                this.allowedMissingMethodnames = allowedMissingMethodnames;
                return this;
            }
            @Override
            public List<DepNode> getDependancies(DepNode node, List<DepNode> allDepNodes) {

                String[] hardDeps = node.a.hardDepends();
                String[] softDeps = node.a.softDepends();
                List<DepNode> res = new ArrayList<>();
                for (String hardDep : hardDeps) {
                    DepNode other = findDepNode(hardDep, allDepNodes);
                    if (other == null && !allowedMissingMethodnames.contains(hardDep))
                        throw new RuntimeException("Test Method \""+node.m.getName()+"\" hard depends on unknown method \""+hardDep+"\"");
                    if (other != null)
                        res.add(other);
                }
                for (String softDep : softDeps) {
                    DepNode other = findDepNode(softDep, allDepNodes);
                    if (other == null && !allowedMissingMethodnames.contains(softDep))
                        throw new RuntimeException("Test Method \""+node.m.getName()+"\" soft depends on unknown method \""+softDep+"\"");
                    if (other != null)
                        res.add(other);
                }
                return res;
            }
        }.init(allMethodNames);

        DependancyRetriever hardDependancyRetriever = new DependancyRetriever() {
            List<String> allowedMissingMethodnames;
            public DependancyRetriever init(List<String> allowedMissingMethodnames) {
                this.allowedMissingMethodnames = allowedMissingMethodnames;
                return this;
            }

            @Override
            public List<DepNode> getDependancies(DepNode node, List<DepNode> allDepNodes) {
                String[] hardDeps = node.a.hardDepends();
                List<DepNode> res = new ArrayList<>();
                for (String hardDep : hardDeps) {
                    DepNode other = findDepNode(hardDep, allDepNodes);
                    if (other == null && !allowedMissingMethodnames.contains(hardDep))
                        throw new RuntimeException("Test Method \""+node.m.getName()+"\" depends on unknown method \""+hardDep+"\"");
                    if (other != null)
                        res.add(other);
                }
                return res;
            }
        }.init(allMethodNames);

        DependancyRetriever allowingSoftDependancyRetriever = (node, allDepNodes) -> {
            String[] softDeps = node.a.softDepends();
            List<DepNode> res = new ArrayList<>();
            for (String softDep : softDeps) {
                DepNode other = findDepNode(softDep, allDepNodes);
                //don't care about missing emthodnames  if (other == null) throw new RuntimeException("Test Method \""+node.m.getName()+"\" depends on unknown method \""+hardDep+"\"");
                if (other != null)
                    res.add(other);
            }
            return res;
        };


        //sort test list using dependencies
        if (group != null)
            testMethods = filterUsingGroup(testMethods, hardDependancyRetriever, group);
        DepNodeSortResult hardSortRes = sortUsingDependancies(testMethods, bothDependancyRetriever);
        if (hardSortRes.hasCycle) {
            List<Method> cycleMethods = hardSortRes.methods.get(hardSortRes.methods.size() - 1);
            String cycleMethodsString = "";
            for (Method m : cycleMethods) {
                if (!cycleMethodsString.isEmpty()) cycleMethodsString += "\n";
                cycleMethodsString += " - "+m.getName();
            }
            throw new RuntimeException("There are cycles in the dependency graph! Methods in cycle:\n"+cycleMethodsString);
        }

        List<Method> sortedTestMethods = new ArrayList<>();
        for (List<Method> subList : hardSortRes.methods) {
//            System.out.println("New hard order");
//            DepNodeSortResult softSortRes = sortUsingDependancies(subList, softDependancyRetriever);
//            //cycles are ignored in soft deps
//            for (List<Method> softSubList : softSortRes.methods) {
//                if (softSortRes.methods.size() > 1)
//                    System.out.println("    New soft order");
//                for (Method m : softSubList) {
//                    System.out.println("        method "+m.getName());
//                    sortedTestMethods.add(m);
//                }
//            }
            sortedTestMethods.addAll(subList);
        }





        //run test setup
        //TODO probably better if we treat setup as just another test, instead of this copy-paste
        final ApiTestResult.ApiTestMethodResult setupRes = new ApiTestResult.ApiTestMethodResult();

        ResultListener setupResultListener =
                result -> setupRes.apiCallDetails.add(
                        serializableApiCallDetailsFactory.createFromApiCallDetails(result)
                );
        test.getLogger().addResultListener(setupResultListener);

        test.setCurrentTestResult(setupRes);
        setupRes.methodName = "setUp";
        setupRes.description = "Test Class setup";
        setupRes.softDependencies = new ArrayList<>();
        setupRes.hardDependencies = new ArrayList<>();
        setupRes.startTimeMs = System.currentTimeMillis();
        setupRes.stopTimeMs = 0;
        try{
            setupRes.state = ApiTestResult.TestResultState.SUCCESS;
            if (!fake)
                test.setUp();
            else
                test.note("\"fake\" set, setup not really called");
            setupRes.stopTimeMs = System.currentTimeMillis();
        } catch (AssertionError ex) {
            setupRes.stopTimeMs = System.currentTimeMillis();
            setupRes.exception = ex;
            setupRes.addLogLine(LogOutput.LogLineType.EXCEPTION, "Caught AssertionError while executing tst class setup: "+ex.getMessage());
            setupRes.state = ApiTestResult.TestResultState.FAILED;
        } catch (Exception ex) {
            setupRes.stopTimeMs = System.currentTimeMillis();
            setupRes.exception = ex;
            setupRes.addLogLine(LogOutput.LogLineType.EXCEPTION, "Caught exception while executing tst class setup: "+ex.getMessage());
            setupRes.state = ApiTestResult.TestResultState.FAILED;
        }
        setupRes.timeMs = setupRes.stopTimeMs - setupRes.startTimeMs;
        test.getLogger().removeResultListener(setupResultListener);

        ApiTestResult result = new ApiTestResult(testedAuthority, user, test.getClass(), group);
        result.resultList.add(setupRes);
        if (!Objects.equals(setupRes.getState(), ApiTestResult.TestResultState.SUCCESS)) {
            //abort testing if setup fails
            result.returnValue = 2;
            listener.onAllTestDone(result, sortedTestMethods.size());
            return result;
        }







        //run them
        boolean allSuccessful = true;
        boolean allMaxWarn = true;
        //this concept (isReturnValue annotation) is under revision
//        boolean singleTestSuccessUsed = false;
//        boolean singleTestSuccess = true;

        List<String> failList = new ArrayList<>();

        int testNr = 1;
//        for (Map.Entry<Method, ApiTest.Test> e : testMethods.entrySet()) {
        for (Method m : sortedTestMethods) {
            ApiTest.Test a = testMethods.get(m);
//            Method m = e.getKey();
//            ApiTest.Test a = e.getValue();

            ApiTestResult.ApiTestMethodResult res = new ApiTestResult.ApiTestMethodResult();

            boolean skip = false;
            for (String hardDep : a.hardDepends())
                if (failList.contains(hardDep)) {
                    res.addLogLine(LogOutput.LogLineType.NOTE, "Test method skipped because hard dependency has failed: "+hardDep);
                    skip = true;
                }

            if (!Objects.equals(a.dataProvider(), "")) {
                LOG.error("TODO: implement dataProvider support or rework tests and remove it");
                res.addLogLine(LogOutput.LogLineType.NOTE, "Test method skipped because it uses dataProvider which is currently not supported. (needs to be implemented)");
                skip = true;
            }

            listener.onStart(m.getName(), testNr, sortedTestMethods.size());
            if (!skip) {
                test.setErrorsFatal();
                res = runSingleTest(test, m, a, fake, serializableApiCallDetailsFactory);
            } else {
                //skip
                res.methodName = m.getName();
                res.description = a.description();
                res.softDependencies = new ArrayList<>(Arrays.asList(a.softDepends()));
                res.hardDependencies = new ArrayList<>(Arrays.asList(a.hardDepends()));
                res.startTimeMs = System.currentTimeMillis();
                res.stopTimeMs = System.currentTimeMillis();
                res.timeMs = 0;
                res.exception = null;
                res.state = ApiTestResult.TestResultState.SKIPPED;
            }
            result.resultList.add(res);
            //this concept (isReturnValue annotation) is under revision
//            if (a.isReturnValue()) {
//                assert !singleTestSuccessUsed : "behaviour singleTestSuccessUsed in case of multiple tests using it is not specified yet";
//                singleTestSuccessUsed = true;
//                singleTestSuccess = res.state.equals(ApiTestResult.TestResultState.SUCCESS);
//            }
            boolean skipHardDeps = false;
            switch (res.state) {
                case FAILED: { allSuccessful = false; allMaxWarn = false; skipHardDeps = true; break; }
                case SKIPPED: { allSuccessful = false; allMaxWarn = false; skipHardDeps = true; break; }
                case WARN: { allSuccessful = false; break; }
                default: break;
            }
            if (skipHardDeps) failList.add(m.getName());

            try {
                listener.onResult(res, testNr, sortedTestMethods.size());
            } catch (AssertionError t) {
                LOG.error("AssertionError in RunTests.runTest listener.onResult: will be ignored as it won't affect the tests, but it is likely a bug.", t);
            } catch (Exception t) {
                LOG.error("Exception in RunTests.runTest listener.onResult: will be ignored as it won't affect the tests, but it is likely a bug.", t);
            }
            testNr++;
        }

        //retval:
        //  2: errors
        //  1: warnings but no errors
        //  0: all success
        int retValue = allMaxWarn ? 1 : 2;
        if (allSuccessful) retValue = 0;

        //this concept (isReturnValue annotation) is under revision
//        if (singleTestSuccessUsed) retValue = singleTestSuccess ? 0 : 2;

        result.returnValue = retValue;
//        System.out.println("Debug return value: allMaxWarn="+allMaxWarn+" allSuccessful="+allSuccessful+
//                " singleTestSuccessUsed="+singleTestSuccessUsed+" singleTestSuccess="+singleTestSuccess+
//                " retValue="+retValue);

        try {
            listener.onAllTestDone(result, sortedTestMethods.size());
        } catch (AssertionError t) {
            LOG.error("AssertionError in RunTests.runTest listener.onAllTestDone: will be ignored as it won't affect the tests, but it is likely a bug.", t);
        } catch (Exception t) {
            LOG.error("Exception in RunTests.runTest listener.onAllTestDone: will be ignored as it won't affect the tests, but it is likely a bug.", t);
        }

        return result;
    }

    private static ApiTestResult.ApiTestMethodResult runSingleTest(
            ApiTest test, Method method, ApiTest.Test annotation, boolean fake,
            SerializableApiCallDetailsFactory serializableApiCallDetailsFactory) {
        final ApiTestResult.ApiTestMethodResult res = new ApiTestResult.ApiTestMethodResult();

        ResultListener resultListener = result -> res.apiCallDetails.add(
                serializableApiCallDetailsFactory.createFromApiCallDetails(result)
        );
        test.getLogger().addResultListener(resultListener);

        test.setCurrentTestResult(res);
        res.methodName = method.getName();
        res.description = annotation.description();
        res.softDependencies = new ArrayList<>(Arrays.asList(annotation.softDepends()));
        res.hardDependencies = new ArrayList<>(Arrays.asList(annotation.hardDepends()));
        res.startTimeMs = System.currentTimeMillis();
        res.stopTimeMs = 0;

        test.setErrorsFatal();
        try{
            try {
                res.state = ApiTestResult.TestResultState.SUCCESS;
                if (!fake) {
                    Object[] args = new Object[0]; //null would also be allowed for invoke
                    method.invoke(test, args);
                }
                else
                    test.note("\"fake\" set: method not really called");
                res.stopTimeMs = System.currentTimeMillis();
            } catch (IllegalAccessException e1) {
                LOG.error("RunTest bug: IllegalAccessException in runSingleTest", e1);
                e1.printStackTrace();
                res.exception = e1;
            } catch (InvocationTargetException e1) {
                if (e1.getCause() != null) {
                    if (e1.getCause() instanceof Exception)
                        throw (Exception) e1.getCause();
                    else {
                        if (e1.getCause() instanceof Error)
                            throw (Error) e1.getCause();
                        else {
                            LOG.error("Throwable is neither Exception or Error?!?", e1.getCause());
                            throw e1;
                        }
                    }
                } else {
                    LOG.error("RunTest bug: InvocationTargetException in runSingleTest", e1);
                    e1.printStackTrace();
                    res.exception = e1;
                    throw e1;
                }
            }
        } catch (SkipTestException ex) {
            res.stopTimeMs = System.currentTimeMillis();
            res.exception = ex;
            res.state = ApiTestResult.TestResultState.SKIPPED;
            if (Objects.equals(ex.getMessage(), ""))
                res.addLogLine(LogOutput.LogLineType.NOTE, "Test method requested test to be skipped");
            else
                res.addLogLine(LogOutput.LogLineType.NOTE, "Test method requested test to be skipped, reason: "+ex.getMessage());
        } catch (AssertionError ex) { //AssertionError is used to signal fatal errors
            res.stopTimeMs = System.currentTimeMillis();
            res.exception = ex;
            res.addLogLine(LogOutput.LogLineType.FATAL_ERROR, ex.getMessage() == null ? "" : ex.getMessage());
            res.state = ApiTestResult.TestResultState.FAILED;
        } catch (JFedException ex) {
            res.stopTimeMs = System.currentTimeMillis();
            res.exception = ex;
            res.addLogLine(LogOutput.LogLineType.EXCEPTION, "Caught exception while executing test: "+ex.getMessage());
            res.state = ApiTestResult.TestResultState.FAILED;

            //try to log as much of the call as possible
            if (ex.getXmlRpcResult() != null) {
                XMLRPCCallDetails xmlRes = ex.getXmlRpcResult();


                res.apiCallDetails.add(serializableApiCallDetailsFactory.createFromApiCallDetails(
                        new ApiCallDetails(null, null, xmlRes.getConnectionConfig(), xmlRes.getServerUrl(),
                                null/*connectionId*/, null, null, null, null,
                        xmlRes.getRequestHttpRequestLine(), xmlRes.getRequestHttpHeaders(), xmlRes.getRequestHttpContent(),
                        xmlRes.getResultHttpStatusLine(), xmlRes.getResultHttpHeaders(), xmlRes.getResultHttpContent(),
                        xmlRes.getRequest(), xmlRes.getResultValueObject(),
                                null, xmlRes.getStartTime(), xmlRes.getStopTime(), xmlRes.getException())
                ));
            }
        } catch (Exception ex) {
            res.stopTimeMs = System.currentTimeMillis();
            res.exception = ex;
            res.addLogLine(LogOutput.LogLineType.EXCEPTION, "Caught exception while executing test: "+ex.getMessage());
            res.state = ApiTestResult.TestResultState.FAILED;
        }
        //Throwable can only be Error now (and not AssertionError), and we don't want to catch real bad Error's here.
//        catch (Error error) {
//            res.stopTimeMs = System.currentTimeMillis();
//            res.exception = error;
//            res.addLogLine(LogOutput.LogLineType.EXCEPTION, "Caught error while executing test: "+error.getMessage());
//            res.state = ApiTestResult.TestResultState.FAILED;
//            throw error;
//        }
        test.setErrorsFatal();
        res.timeMs = res.stopTimeMs - res.startTimeMs;
        test.getLogger().removeResultListener(resultListener);
        return res;
    }

}
