/**
 * 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.handlers;

import static java.util.Objects.requireNonNull;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Route;
import org.jooby.Route.Chain;
import org.jooby.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;

/**
 * Handle preflight and simple CORS requests. CORS options are set via: {@link Cors}.
 *
 * @author edgar
 * @since 0.8.0
 * @see Cors
 */
public class CorsHandler implements Route.Filter {

  private static final String ORIGIN = "Origin";

  private static final String ANY_ORIGIN = "*";

  private static final String AC_REQUEST_METHOD = "Access-Control-Request-Method";

  private static final String AC_REQUEST_HEADERS = "Access-Control-Request-Headers";

  private static final String AC_MAX_AGE = "Access-Control-Max-Age";

  private static final String AC_EXPOSE_HEADERS = "Access-Control-Expose-Headers";

  private static final String AC_ALLOW_ORIGIN = "Access-Control-Allow-Origin";

  private static final String AC_ALLOW_HEADERS = "Access-Control-Allow-Headers";

  private static final String AC_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";

  private static final String AC_ALLOW_METHODS = "Access-Control-Allow-Methods";

  /** The logging system. */
  private final Logger log = LoggerFactory.getLogger(Cors.class);

  private Optional<Cors> cors = Optional.empty();

  /**
   * Creates a new {@link CorsHandler}.
   *
   * @param cors Cors options, or empty for using default options.
   */
  public CorsHandler(final Cors cors) {
    this.cors = Optional.of(requireNonNull(cors, "Cors is required."));
  }

  /**
   * Creates a new {@link CorsHandler}.
   */
  public CorsHandler() {
  }

  @Override
  public void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
    Optional<String> origin = req.header("Origin").toOptional();
    Cors cors = this.cors.orElseGet(() -> req.require(Cors.class));
    if (cors.enabled() && origin.isPresent()) {
      cors(cors, req, rsp, origin.get());
    }
    chain.next(req, rsp);
  }

  private void cors(final Cors cors, final Request req, final Response rsp,
      final String origin) throws Exception {
    if (cors.allowOrigin(origin)) {
      log.debug("allowed origin: {}", origin);
      if (preflight(req)) {
        log.debug("handling preflight for: {}", origin);
        preflight(cors, req, rsp, origin);
      } else {
        log.debug("handling simple cors for: {}", origin);
        if ("null".equals(origin)) {
          rsp.header(AC_ALLOW_ORIGIN, ANY_ORIGIN);
        } else {
          rsp.header(AC_ALLOW_ORIGIN, origin);
          if (!cors.anyOrigin()) {
            rsp.header("Vary", ORIGIN);
          }
          if (cors.credentials()) {
            rsp.header(AC_ALLOW_CREDENTIALS, true);
          }
          if (!cors.exposedHeaders().isEmpty()) {
            rsp.header(AC_EXPOSE_HEADERS, join(cors.exposedHeaders()));
          }
        }
      }
    }
  }

  private boolean preflight(final Request req) {
    return req.method().equals("OPTIONS") && req.header(AC_REQUEST_METHOD).isSet();
  }

  private void preflight(final Cors cors, final Request req, final Response rsp,
      final String origin) {
    /**
     * Allowed method
     */
    boolean allowMethod = req.header(AC_REQUEST_METHOD).toOptional()
        .map(cors::allowMethod)
        .orElse(false);
    if (!allowMethod) {
      return;
    }

    /**
     * Allowed headers
     */
    List<String> headers = req.header(AC_REQUEST_HEADERS).toOptional().map(header ->
        Splitter.on(',').trimResults().omitEmptyStrings().splitToList(header)
        ).orElse(Collections.emptyList());
    if (!cors.allowHeaders(headers)) {
      return;
    }

    /**
     * Allowed methods
     */
    rsp.header(AC_ALLOW_METHODS, join(cors.allowedMethods()));

    List<String> allowedHeaders = cors.anyHeader() ? headers : cors.allowedHeaders();
    rsp.header(AC_ALLOW_HEADERS, join(allowedHeaders));

    /**
     * Allow credentials
     */
    if (cors.credentials()) {
      rsp.header(AC_ALLOW_CREDENTIALS, true);
    }

    if (cors.maxAge() > 0) {
      rsp.header(AC_MAX_AGE, cors.maxAge());
    }

    rsp.header(AC_ALLOW_ORIGIN, origin);

    if (!cors.anyOrigin()) {
      rsp.header("Vary", ORIGIN);
    }

    rsp.status(Status.OK).end();
  }

  private String join(final List<String> values) {
    return Joiner.on(',').join(values);
  }

}
