package com.xebialabs.restito.semantics;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.function.Function;

import com.xebialabs.restito.support.file.FileHelper;
import com.xebialabs.restito.support.resource.ResourceHelper;
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.server.Response;
import org.glassfish.grizzly.http.util.HttpStatus;

/**
 * Action is a modifier for Response
 *
 * @see org.glassfish.grizzly.http.server.Response
 */
public class Action implements Applicable {

    private Function<Response, Response> function;

    private Action(Function<Response, Response> function) {
        this.function = function;
    }

    /**
     * Perform the action with response
     */
    @Override
    public Response apply(Response r) {
        return this.function.apply(r);
    }

    // Factory methods

    /**
     * Sets HTTP status 200 to response.
     * Consider using {@link #ok()} as more concise and less ambiguous option.
     * @see #ok()
     */
    public static Action success() {
        return status(HttpStatus.OK_200);
    }

    /**
     * Sets HTTP status 200 to response
     */
    public static Action ok() {
        return status(HttpStatus.OK_200);
    }

    /**
     * Sets HTTP status 204 to response
     */
    public static Action noContent() {
        return status(HttpStatus.NO_CONTENT_204);
    }

    /**
     * Sets HTTP status to response
     */
    public static Action status(final HttpStatus status) {
        return new Action(input -> {
            input.setStatus(status);
            return input;
        });
    }

    /**
     * Writes content and content-type of resource file to response.
     * Tries to detect content type based on file extension. If can not detect - content-type is not set.
     * For now there are following bindings:
     * <ul>
     * <li>.xml - `application/xml`</li>
     * <li>.json - `application/xml`</li>
     * </ul>
     */
    public static Action resourceContent(final String resourcePath) {
        return new Action(input -> {
            final HttpResponsePacket responsePacket = input.getResponse();
            String encoding = responsePacket == null ? input.getCharacterEncoding() : responsePacket.getCharacterEncoding();
            return resourceContent(resourcePath, encoding).apply(input);
        });
    }

    /**
     * Combines {@link #resourceContent(String)} and {@link #charset(String)}
     */
    public static Action resourceContent(String resourcePath, String charset) {
        URL resource = Action.class.getClassLoader().getResource(resourcePath);
        if (resource == null) {
            throw new IllegalArgumentException(String.format("Resource %s not found.", resourcePath));
        }
        Action charsetAction = charset == null ? noop() : charset(Charset.forName(charset));
        return composite(resourceContent(resource), charsetAction);
    }

    /**
     * Writes content using the specified encoding and content-type of resource file to response.
     * Tries to detect content type based on file extension. If can not detect, content-type is not set.
     * For now there are following bindings:
     * <ul>
     * <li>.xml - `application/xml`</li>
     * <li>.json - `application/xml`</li>
     * </ul>
     */
    public static Action resourceContent(URL resourceUrl) {
        try {
            final byte[] bytes = ResourceHelper.getBytes(resourceUrl);

            String fileExtension = FileHelper.getFileExtension(resourceUrl.getPath());

            Action mainAction = bytesContent(bytes);

            if (fileExtension.equalsIgnoreCase("xml")) {
                mainAction = composite(contentType("application/xml"), mainAction);
            } else if (fileExtension.equalsIgnoreCase("json")) {
                mainAction = composite(contentType("application/json"), mainAction);
            }

            return mainAction;
        } catch (IOException e) {
            throw new RuntimeException("Can not read resource for restito stubbing.");
        }
    }

    /**
     * Combines {@link #resourceContent(java.net.URL)} and {@link #charset(Charset)}
     */
    public static Action resourceContent(URL resourceUrl, Charset charset) {
        final Action charsetAction = charset != null ? charset(charset) : Action.noop();
        return composite(resourceContent(resourceUrl), charsetAction);
    }

    /**
     * Writes bytes content to response
     */
    public static Action bytesContent(final byte[] content) {
        return new Action(response -> {

            response.setContentLength(content.length);
            try {
                response.getOutputStream().write(content);
            } catch (IOException e) {
                throw new RuntimeException("Can not write resource content for restito stubbing.");
            }
            return response;
        });
    }

    /**
     * Writes string content into response. Important: when using this method, the string gets encoded
     * into a sequence of bytes using the platform's default charset. Please use {@link #stringContent(String, Charset)}
     * if you need to be specific about the encoding.
     */
    public static Action stringContent(final String content) {
        return bytesContent(content.getBytes());
    }

    /**
     * Writes string content into response. The passed charset is used:
     *  - To encoded the string into bytes;
     *  - As character set of the response.
     * The latter will result in a <pre>charset=XXX</pre> part of <pre>Content-Type</pre> header
     * if and only if the header has been set in some way (e.g. by using {@link #contentType(String)} action.
     *
     * See `integration.CharsetAndEncodingTest` for more details.
     */
    public static Action stringContent(final String content, final Charset charset) {
        return composite(charset(charset), bytesContent(content.getBytes(charset)));
    }

    /**
     * Sets key-value header on response
     */
    public static Action header(final String key, final String value) {
        return new Action(input -> {
            input.setHeader(key, value);
            return input;
        });
    }

    /**
     * Sets content type to the response
     */
    public static Action contentType(final String contentType) {
        return new Action(r -> {
            r.setContentType(contentType);
            return r;
        });
    }

    /**
     * Sets charset of the response (must come before stringContent/resourceContent Action)
     */
    public static Action charset(final String charset) {
        return new Action(r -> {
            r.setCharacterEncoding(charset);
            return r;
        });
    }

    /**
     * Sets charset of the response (must come before stringContent/resourceContent Action)
     */
    public static Action charset(final Charset charset) {
        return charset(charset.name());
    }

    /**
     * Returns unauthorized response with default realm name
     */
    public static Action unauthorized() {
        return unauthorized("Restito realm");
    }

    /**
     * Returns unauthorized response
     */
    public static Action unauthorized(final String realm) {
        return new Action(r -> {
            r.addHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
            r.setStatus(HttpStatus.UNAUTHORIZED_401);
            return r;
        });
    }

    /**
     * Perform set of custom actions on response
     */
    public static Action custom(Function<Response, Response> f) {
        return new Action(f);
    }

    /**
     * Creates a composite action which contains all passed actions and
     * executes them in the same order.
     */
    public static Action composite(final Applicable... actions) {
        return new Action(input -> {
            for (Applicable action : actions) {
                action.apply(input);
            }

            return input;
        });
    }

    /**
     * Creates a composite action which contains all passed actions and
     * executes them in the same order.
     */
    public static Action composite(final Collection<Applicable> applicables) {
        return new Action(input -> {
            for (Applicable action : applicables) {
                action.apply(input);
            }

            return input;
        });
    }

    /**
     * Creates a composite action which contains all passed actions and
     * executes them in the same order.
     */
    public static Action composite(final Action... actions) {
        return new Action(input -> {
            for (Applicable action : actions) {
                action.apply(input);
            }

            return input;
        });
    }

    /**
     * Doing nothing. To be used in DSLs for nicer syntax.
     */
    public static Action noop() {
        return new Action(input -> input);
    }

    /**
     * Sleeps so many milliseconds, emulating slow requests.
     */
    public static Action delay(final Integer delay) {
        return new Action(input -> {
            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return input;
        });
    }
}
