/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.jooby.internal;

import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jooby.Parser;
import org.jooby.Upload;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.inject.TypeLiteral;

@SuppressWarnings({"unchecked", "rawtypes" })
public enum BuiltinParser implements Parser {

  Basic {
    private final Map<Class<?>, Function<String, Object>> parsers = ImmutableMap
        .<Class<?>, Function<String, Object>> builder()
        .put(BigDecimal.class, NOT_EMPTY.andThen(BigDecimal::new))
        .put(BigInteger.class, NOT_EMPTY.andThen(BigInteger::new))
        .put(Byte.class, NOT_EMPTY.andThen(Byte::valueOf))
        .put(byte.class, NOT_EMPTY.andThen(Byte::valueOf))
        .put(Double.class, NOT_EMPTY.andThen(Double::valueOf))
        .put(double.class, NOT_EMPTY.andThen(Double::valueOf))
        .put(Float.class, NOT_EMPTY.andThen(Float::valueOf))
        .put(float.class, NOT_EMPTY.andThen(Float::valueOf))
        .put(Integer.class, NOT_EMPTY.andThen(Integer::valueOf))
        .put(int.class, NOT_EMPTY.andThen(Integer::valueOf))
        .put(Long.class, NOT_EMPTY.andThen(this::toLong))
        .put(long.class, NOT_EMPTY.andThen(this::toLong))
        .put(Short.class, NOT_EMPTY.andThen(Short::valueOf))
        .put(short.class, NOT_EMPTY.andThen(Short::valueOf))
        .put(Boolean.class, NOT_EMPTY.andThen(this::toBoolean))
        .put(boolean.class, NOT_EMPTY.andThen(this::toBoolean))
        .put(Character.class, NOT_EMPTY.andThen(this::toCharacter))
        .put(char.class, NOT_EMPTY.andThen(this::toCharacter))
        .put(String.class, this::toString)
        .build();

    @Override
    public Object parse(final TypeLiteral<?> type, final Parser.Context ctx) throws Throwable {
      Function<String, Object> parser = parsers.get(type.getRawType());
      if (parser != null) {
        return ctx
            .param(values -> parser.apply(values.get(0))).body(body -> parser.apply(body.text()));
      }
      return ctx.next();
    }

    private String toString(final String value) {
      return value;
    }

    private char toCharacter(final String value) {
      return value.charAt(0);
    }

    private Boolean toBoolean(final String value) {
      if ("true".equals(value)) {
        return Boolean.TRUE;
      } else if ("false".equals(value)) {
        return Boolean.FALSE;
      }
      throw new IllegalArgumentException("Not a boolean: " + value);
    }

    private Long toLong(final String value) {
      try {
        return Long.valueOf(value);
      } catch (NumberFormatException ex) {
        // long as date, like If-Modified-Since
        try {
          LocalDateTime date = LocalDateTime.parse(value, Headers.fmt);
          Instant instant = date.toInstant(ZoneOffset.UTC);
          return instant.toEpochMilli();
        } catch (DateTimeParseException ignored) {
          throw ex;
        }
      }

    }
  },

  Collection {
    private final Map<Class<?>, Supplier<ImmutableCollection.Builder<?>>> parsers = ImmutableMap.<Class<?>, Supplier<ImmutableCollection.Builder<?>>> builder()
        .put(List.class, ImmutableList.Builder::new)
        .put(Set.class, ImmutableSet.Builder::new)
        .put(SortedSet.class, ImmutableSortedSet::naturalOrder)
        .build();

    private boolean matches(final TypeLiteral<?> toType) {
      return parsers.containsKey(toType.getRawType())
          && toType.getType() instanceof ParameterizedType;
    }

    @Override
    public Object parse(final TypeLiteral<?> type, final Parser.Context ctx) throws Throwable {
      if (matches(type)) {
        return ctx.param(values -> {
          ImmutableCollection.Builder builder = parsers.get(type.getRawType()).get();
          TypeLiteral<?> paramType = TypeLiteral.get(((ParameterizedType) type.getType())
              .getActualTypeArguments()[0]);
          for (Object value : values) {
            builder.add(ctx.next(paramType, value));
          }
          return builder.build();
        }).upload(uploads -> {
          ImmutableCollection.Builder builder = parsers.get(type.getRawType()).get();
          TypeLiteral<Upload> paramType = TypeLiteral.get(Upload.class);
          for (Upload upload : uploads) {
            builder.add(ctx.next(paramType, upload));
          }
          return builder.build();
        });
      } else {
        return ctx.next();
      }
    }
  },

  Optional {
    private boolean matches(final TypeLiteral<?> toType) {
      return Optional.class == toType.getRawType() && toType.getType() instanceof ParameterizedType;
    }

    @Override
    public Object parse(final TypeLiteral<?> type, final Parser.Context ctx)
        throws Throwable {
      if (matches(type)) {
        TypeLiteral<?> paramType = TypeLiteral.get(((ParameterizedType) type.getType())
            .getActualTypeArguments()[0]);
        return ctx
            .param(values -> {
              if (values.size() == 0) {
                return java.util.Optional.empty();
              }
              return java.util.Optional.of(ctx.next(paramType));
            }).body(body -> {
              if (body.length() == 0) {
                return java.util.Optional.empty();
              }
              return java.util.Optional.of(ctx.next(paramType));
            }).upload(files -> {
              return java.util.Optional.of(ctx.next(paramType));
            });
      } else {
        return ctx.next();
      }
    }
  },

  Enum {
    @Override
    public Object parse(final TypeLiteral<?> type, final Parser.Context ctx)
        throws Throwable {
      Class rawType = type.getRawType();
      if (Enum.class.isAssignableFrom(rawType)) {
        return ctx
            .param(values -> toEnum(rawType, values.get(0)))
            .body(body -> toEnum(rawType, body.text()));
      } else {
        return ctx.next();
      }
    }

    Object toEnum(final Class type, final String value) {
      Set<Enum> set = EnumSet.allOf(type);
      return set.stream()
          .filter(e -> e.name().equalsIgnoreCase(value))
          .findFirst()
          .orElseGet(() -> java.lang.Enum.valueOf(type, value));
    }
  },

  Upload {
    @Override
    public Object parse(final TypeLiteral<?> type, final Context ctx) throws Throwable {
      if (Upload.class == type.getRawType()) {
        return ctx.upload(uploads -> uploads.get(0));
      } else {
        return ctx.next();
      }
    }
  },

  Bytes {
    @Override
    public Object parse(final TypeLiteral<?> type, final Parser.Context ctx) throws Throwable {
      if (type.getRawType() == byte[].class) {
        return ctx.body(body -> body.bytes());
      }
      return ctx.next();
    }

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

}
