/*
 * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Additionally licensed with:
 *
 * 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.spf4j.base;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.spf4j.avro.Configs;
import org.spf4j.base.avro.ObjectPattern;
import org.spf4j.base.avro.ThrowablePattern;
import org.spf4j.base.avro.OperationsResultPatterns;
import org.spf4j.base.avro.OperationResultPatterns;
import org.spf4j.ds.CopyOnWriteMap;

/**
 * @author Zoltan Farkas
 */
public final class ResultMatchers {

  private static final Map<String, Either<Predicate<Throwable>, Predicate<Object>>> RESULT_MATCHERS;

  static {
    try {
      RESULT_MATCHERS  = new CopyOnWriteMap<>(getRegisteredResultMatchers());
    } catch (IOException ex) {
      throw new ExceptionInInitializerError(ex);
    }
  }

  public interface Supplier extends Function<String, Either<Predicate<Throwable>, Predicate<Object>>> {
    default Supplier chain(final Supplier fallback) {
      Supplier current = this;
      return t -> {
        Either<Predicate<Throwable>, Predicate<Object>> result = current.apply(t);
        if (result != null) {
          return result;
        } else {
          return fallback.apply(t);
        }
      };
    }
  }

  private ResultMatchers() { }


  private static Map<String, Either<Predicate<Throwable>, Predicate<Object>>> getRegisteredResultMatchers()
          throws IOException {
    Map<String, Either<Predicate<Throwable>, Predicate<Object>>> reasons = new HashMap<>();
    for (ThrowableMatcher reason : ServiceLoader.load(ThrowableMatcher.class)) {
      reasons.put(reason.getOperationName(), Either.left(reason));
    }
    for (Enumeration<URL> e = ClassLoader.getSystemResources("result_matchers.json"); e.hasMoreElements();) {
      URL url = e.nextElement();
      try (InputStream is = url.openStream();
              Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
        Configs.read(OperationsResultPatterns.class, reader);
        OperationsResultPatterns patterns = Configs.read(OperationsResultPatterns.class, reader);
        for (OperationResultPatterns opPat : patterns.getPatterns().values()) {
          add(reasons, opPat.getThrowablePatterns(), true);
          add2(reasons, opPat.getReturnPatterns(), true);
        }
      }
    }
    return reasons;
  }

  public static Map<String, Either<Predicate<Throwable>, Predicate<Object>>> operationsFromConfigValue(
          final String value) {
    Map<String, Either<Predicate<Throwable>, Predicate<Object>>> reasons = new HashMap<>();
    try (Reader reader = new StringReader(value)) {
      OperationsResultPatterns patterns = Configs.read(OperationsResultPatterns.class, reader);
      for (OperationResultPatterns opPat : patterns.getPatterns().values()) {
        add(reasons, opPat.getThrowablePatterns(), true);
        add2(reasons, opPat.getReturnPatterns(), true);
      }
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
    return reasons;
  }

  public static Map<String, Either<Predicate<Throwable>, Predicate<Object>>> operationfromConfigValue(
          final String value) {
    Map<String, Either<Predicate<Throwable>, Predicate<Object>>> reasons = new HashMap<>();
    try (Reader reader = new StringReader(value)) {
      OperationResultPatterns patterns = Configs.read(OperationResultPatterns.class, reader);
      add(reasons, patterns.getThrowablePatterns(), true);
      add2(reasons, patterns.getReturnPatterns(), true);
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
    return reasons;
  }

  @Nullable
  public static Either<Predicate<Throwable>, Predicate<Object>> getThrowableResultMatcher(final String reason) {
    return RESULT_MATCHERS.get(reason);
  }

  public static Supplier toSupplier() {
    return ResultMatchers::getThrowableResultMatcher;
  }

  public static Predicate<Throwable> toPredicate(final ThrowablePattern pattern) throws ClassNotFoundException {
    Class<? extends Throwable> clasz = (Class) Class.forName(pattern.getType());
    Pattern regex = pattern.getMsgPattern();
    Map<String, List<Integer>> codes = pattern.getCodes();
    return new ThrowablePredicate(clasz, regex, codes);
  }

  public static Predicate<Object> toPredicate(final ObjectPattern pattern) throws ClassNotFoundException {
    Class<? extends Object> clasz = (Class) Class.forName(pattern.getType());
    Map<String, List<Integer>> codes = pattern.getCodes();
    return new ObjectPredicate(clasz, codes);
  }

  public static void add(final Map<String, Either<Predicate<Throwable>, Predicate<Object>>> predicates,
          final Map<String, ThrowablePattern> patterns, final boolean lenient) {
    for (Map.Entry<String, ThrowablePattern> entry : patterns.entrySet()) {
      ThrowablePattern pattern = entry.getValue();
      String name = entry.getKey();
      try {
         predicates.put(name, Either.left(toPredicate(pattern)));
      } catch (ClassNotFoundException ex) {
        if (lenient) {
          Logger logger = Logger.getLogger(ResultMatchers.class.getName());
          logger.log(Level.WARNING, "Cannot register predicate {0}, class not found: {1}",
                          new Object[] {name, pattern.getType()});
          logger.log(Level.FINE, "Exception detail", ex);
        } else {
          throw new RuntimeException(ex);
        }
      }
    }
  }


  public static void add2(final  Map<String, Either<Predicate<Throwable>, Predicate<Object>>> predicates,
          final Map<String, ObjectPattern> patterns, final boolean lenient) {
    for (Map.Entry<String, ObjectPattern> entry : patterns.entrySet()) {
      ObjectPattern pattern = entry.getValue();
      String name = entry.getKey();
      try {
         predicates.put(name, Either.right(toPredicate(pattern)));
      } catch (ClassNotFoundException ex) {
        if (lenient) {
          Logger logger = Logger.getLogger(ResultMatchers.class.getName());
          logger.log(Level.WARNING, "Cannot register predicate {0}, class not found: {1}",
                          new Object[] {name, pattern.getType()});
          logger.log(Level.FINE, "Exception detail", ex);
        } else {
          throw new RuntimeException(ex);
        }
      }
    }
  }


  @SuppressFBWarnings("DMC_DUBIOUS_MAP_COLLECTION")
  private static class ObjectPredicate implements Predicate<Object> {

    private final Class<? extends Object> clasz;
    private final Map<String, List<Integer>> codes;

    ObjectPredicate(final Class<? extends Object> clasz, final Map<String, List<Integer>> codes) {
      this.clasz = clasz;
      this.codes = codes;
    }

    @Override
    public boolean test(final Object t) {
      Class<? extends Object> tClass = t.getClass();
      if (clasz.isAssignableFrom(tClass)) {
          for (Map.Entry<String, List<Integer>> entry : codes.entrySet()) {
            String getterName = Strings.attributeToMethod("get", entry.getKey());
            Method m = Reflections.getMethod(tClass, getterName);
            if (m == null) {
              return false;
            }
            Object result;
            try {
              result = m.invoke(t);
            } catch (IllegalAccessException | InvocationTargetException ex) {
              throw new RuntimeException(ex);
            }
            boolean hasCode = false;
            for (Integer code : entry.getValue()) {
              if (code.equals(result)) {
                hasCode = true;
                break;
              }
            }
            if (!hasCode) {
              return false;
            }
          }
          return true;
      }
      return false;
    }
  }

  @SuppressFBWarnings("DMC_DUBIOUS_MAP_COLLECTION")
  private static class ThrowablePredicate implements Predicate<Throwable> {

    private final Class<? extends Throwable> clasz;
    private final Pattern messageRegex;
    private final Map<String, List<Integer>> codes;

    ThrowablePredicate(final Class<? extends Throwable> clasz,
            final Pattern messageRegex, final Map<String, List<Integer>> codes) {
      this.clasz = clasz;
      this.messageRegex = messageRegex;
      this.codes = codes;
    }

    @Override
    public boolean test(final Throwable t) {
      Class<? extends Throwable> tClass = t.getClass();
      if (clasz.isAssignableFrom(tClass)) {
        String msg = t.getMessage();
        if (msg == null) {
          msg = "";
        }
        if (messageRegex.matcher(msg).matches()) {
          for (Map.Entry<String, List<Integer>> entry : codes.entrySet()) {
            String getterName = Strings.attributeToMethod("get", entry.getKey());
            Method m = Reflections.getMethod(tClass, getterName);
            if (m == null) {
              return false;
            }
            Object result;
            try {
              result = m.invoke(t);
            } catch (IllegalAccessException | InvocationTargetException ex) {
              throw new RuntimeException(ex);
            }
            boolean hasCode = false;
            for (Integer code : entry.getValue()) {
              if (code.equals(result)) {
                hasCode = true;
                break;
              }
            }
            if (!hasCode) {
              return false;
            }
          }
          return true;
        }
      }
      return false;
    }
  }


}
