/*
 * Copyright (c) 2018 "JackWhite20"
 *
 * This file is part of Cobra.
 *
 * Cobra is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.jackwhite20.cobra.client.impl;

import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import de.jackwhite20.cobra.client.CobraClient;
import de.jackwhite20.cobra.shared.RequestMethod;
import de.jackwhite20.cobra.shared.Status;
import de.jackwhite20.cobra.shared.http.Body;
import de.jackwhite20.cobra.shared.http.Headers;
import de.jackwhite20.cobra.shared.http.Response;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;

/**
 * Created by JackWhite20 on 01.02.2016.
 */
public class CobraClientImpl implements CobraClient {

    private static final int DEFAULT_TIMEOUT = 2000;

    private static final int CONTENT_DISPOSITION_FILENAME_LENGTH = 10;

    private static final String USER_AGENT = "Cobra v2.4.3";

    private int connectTimeout;

    private Gson gson = new Gson();

    private String baseUrl;

    public CobraClientImpl() {
        this(DEFAULT_TIMEOUT, "");
    }

    public CobraClientImpl(String baseUrl) {
        this(DEFAULT_TIMEOUT, baseUrl);
    }

    public CobraClientImpl(int connectTimeout) {
        this(connectTimeout, "");
    }

    public CobraClientImpl(int connectTimeout, String baseUrl) {
        this.connectTimeout = connectTimeout;
        this.baseUrl = baseUrl;
    }

    @Override
    public Response post(URL url, Body body, Headers headers) throws IOException {
        return post(url, null, body, headers);
    }

    @Override
    public Response post(URL url, Proxy proxy, Body body, Headers headers) throws IOException {
        Preconditions.checkNotNull(url, "url cannot be null");
        Preconditions.checkNotNull(body, "body cannot be null");
        Preconditions.checkNotNull(headers, "headers cannot be null");

        HttpHost proxyHost = null;
        if (proxy != null) {
            InetSocketAddress address = (InetSocketAddress) proxy.address();
            proxyHost = new HttpHost(address.getAddress(), address.getPort());
        }

        try (CloseableHttpClient client = HttpClients.custom().setProxy(proxyHost).build()) {
            HttpPost post = new HttpPost(url.toURI());
            post.setHeader("User-Agent", USER_AGENT);
            for (Map.Entry<String, String> entry : headers.headers().entrySet()) {
                post.setHeader(entry.getKey(), entry.getValue());
            }
            post.setEntity(new ByteArrayEntity(body.bytes()));

            CloseableHttpResponse response = client.execute(post);

            Headers responseHeaders = Headers.empty();
            for (Header header : response.getAllHeaders()) {
                responseHeaders.header(header.getName(), header.getValue());
            }

            return new Response(Status.valueOf(response.getStatusLine().getStatusCode()), responseHeaders, Body.of(EntityUtils.toByteArray(response.getEntity())));
        } catch (URISyntaxException | IOException e) {
            e.printStackTrace();
        }

        return Response.status(Status.BAD_REQUEST).build();
    }

    @Override
    public Response post(URL url, Object object, Headers headers) throws IOException {
        return post(url, null, object, headers);
    }

    @Override
    public Response post(URL url, Proxy proxy, Object object, Headers headers) throws IOException {
        Preconditions.checkNotNull(object, "object cannot be null");

        return post(url, proxy, Body.of(gson.toJson(object)), headers);
    }

    @Override
    public Response post(String relativePath, Body body, Headers headers) throws IOException {
        return post(relativePath, null, body, headers);
    }

    @Override
    public Response post(String relativePath, Proxy proxy, Body body, Headers headers) throws IOException {
        return post(buildPath(relativePath), proxy, body, headers);
    }

    @Override
    public Response post(String relativePath, Object body, Headers headers) throws IOException {
        return post(relativePath, null, body, headers);
    }

    @Override
    public Response post(String relativePath, Proxy proxy, Object body, Headers headers) throws IOException {
        return post(buildPath(relativePath), proxy, body, headers);
    }

    @Override
    public Response put(URL url, Headers headers) throws IOException {
        return put(url, null, headers);
    }

    @Override
    public Response put(URL url, Proxy proxy, Headers headers) throws IOException {
        return request(url, proxy, headers, RequestMethod.PUT);
    }

    @Override
    public Response put(String relativePath, Headers headers) throws IOException {
        return put(relativePath, null, headers);
    }

    @Override
    public Response put(String relativePath, Proxy proxy, Headers headers) throws IOException {
        return put(buildPath(relativePath), proxy, headers);
    }

    @Override
    public Response patch(URL url, Headers headers) throws IOException {
        return patch(url, null, headers);
    }

    @Override
    public Response patch(String relativePath, Headers headers) throws IOException {
        return patch(relativePath, null, headers);
    }

    @Override
    public Response patch(URL url, Proxy proxy, Headers headers) throws IOException {
        return request(url, proxy, headers, RequestMethod.PATCH);
    }

    @Override
    public Response patch(String relativePath, Proxy proxy, Headers headers) throws IOException {
        return patch(buildPath(relativePath), proxy, headers);
    }

    @Override
    public Response delete(URL url, Headers headers) throws IOException {
        return delete(url, null, headers);
    }

    @Override
    public Response delete(URL url, Proxy proxy, Headers headers) throws IOException {
        return request(url, proxy, headers, RequestMethod.DELETE);
    }

    @Override
    public Response delete(String relativePath, Headers headers) throws IOException {
        return delete(relativePath, null, headers);
    }

    @Override
    public Response delete(String relativePath, Proxy proxy, Headers headers) throws IOException {
        return delete(buildPath(relativePath), proxy, headers);
    }

    @Override
    public Response get(URL url, Headers headers) throws IOException {
        return get(url, null, headers);
    }

    @Override
    public Response get(URL url, Proxy proxy, Headers headers) throws IOException {
        return request(url, proxy, headers, RequestMethod.GET);
    }

    @Override
    public Response get(String relativePath, Proxy proxy, Headers headers) throws IOException {
        return get(buildPath(relativePath), proxy, headers);
    }

    @Override
    public Response get(String relativePath, Headers headers) throws IOException {
        return get(relativePath, null, headers);
    }

    @Override
    public <T> T get(URL url, Proxy proxy, Headers headers, Class<T> clazz) throws IOException {
        Response response = request(url, proxy, headers, RequestMethod.GET);

        return gson.fromJson(response.body().content(), clazz);
    }

    @Override
    public <T> T get(URL url, Headers headers, Class<T> clazz) throws IOException {
        return get(url, null, headers, clazz);
    }

    @Override
    public <T> T get(String relativePath, Proxy proxy, Headers headers, Class<T> clazz) throws IOException {
        return get(buildPath(relativePath), proxy, headers, clazz);
    }

    @Override
    public <T> T get(String relativePath, Headers headers, Class<T> clazz) throws IOException {
        return get(relativePath, null, headers, clazz);
    }

    @Override
    public Response download(URL url, Headers headers, String folderToSaveTo) throws IOException {
        return download(url, null, headers, folderToSaveTo);
    }

    @Override
    public Response download(URL url, Proxy proxy, Headers headers, String folderToSaveTo) throws IOException {
        Preconditions.checkNotNull(url, "url cannot be null");
        Preconditions.checkNotNull(headers, "headers cannot be null");
        Preconditions.checkNotNull(folderToSaveTo, "folderToSaveTo cannot be null");
        Preconditions.checkArgument(!folderToSaveTo.isEmpty(), "folderToSaveTo cannot be empty (current working directory can be represented with ./");

        HttpHost proxyHost = null;
        if (proxy != null) {
            InetSocketAddress address = (InetSocketAddress) proxy.address();
            proxyHost = new HttpHost(address.getAddress(), address.getPort());
        }

        try (CloseableHttpClient client = HttpClients.custom().setProxy(proxyHost).build()) {
            HttpGet get = new HttpGet(url.toURI());
            get.setHeader("User-Agent", USER_AGENT);
            for (Map.Entry<String, String> entry : headers.headers().entrySet()) {
                get.setHeader(entry.getKey(), entry.getValue());
            }

            CloseableHttpResponse response = client.execute(get);

            Header dispositionHeader = response.getFirstHeader("Content-Disposition");
            String disposition = dispositionHeader != null ? dispositionHeader.getValue() : null;

            if (disposition == null) {
                throw new IllegalArgumentException("no 'Content-Disposition' header present");
            }

            String fileName = null;

            int index = disposition.indexOf("filename=");
            if (index > 0) {
                fileName = disposition.substring(index + CONTENT_DISPOSITION_FILENAME_LENGTH, disposition.length() - 1);
            }

            if (fileName == null) {
                throw new IllegalStateException("unable to get the file name from the disposition header: " + disposition);
            }

            Headers responseHeaders = Headers.empty();
            for (Header header : response.getAllHeaders()) {
                responseHeaders.header(header.getName(), header.getValue());
            }

            HttpEntity entity = response.getEntity();
            if (entity != null) {
                try (FileOutputStream fileOutputStream = new FileOutputStream(folderToSaveTo + File.separator + fileName)) {
                    entity.writeTo(fileOutputStream);
                }
            }

            return new Response(Status.valueOf(response.getStatusLine().getStatusCode()), responseHeaders, Body.of("".getBytes()));
        } catch (URISyntaxException | IOException e) {
            e.printStackTrace();
        }

        return Response.status(Status.BAD_REQUEST).build();
    }

    @Override
    public Response download(String relativePath, Headers headers, String folderToSaveTo) throws IOException {
        return download(relativePath, null, headers, folderToSaveTo);
    }

    @Override
    public Response download(String relativePath, Proxy proxy, Headers headers, String folderToSaveTo) throws IOException {
        return download(buildPath(relativePath), proxy, headers, folderToSaveTo);
    }

    private Response request(URL url, Proxy proxy, Headers headers, RequestMethod method) throws IOException {
        Preconditions.checkNotNull(url, "url cannot be null");
        Preconditions.checkNotNull(headers, "headers cannot be null");

        HttpHost proxyHost = null;
        if (proxy != null) {
            InetSocketAddress address = (InetSocketAddress) proxy.address();
            proxyHost = new HttpHost(address.getAddress(), address.getPort());
        }

        try (CloseableHttpClient client = HttpClients.custom().setProxy(proxyHost).build()) {
            HttpUriRequest request = new HttpGet(url.toURI());
            if (method == RequestMethod.POST) {
                request = new HttpPost(url.toURI());
            } else if (method == RequestMethod.DELETE) {
                request = new HttpDelete(url.toURI());
            } else if (method == RequestMethod.PATCH) {
                request = new HttpPatch(url.toURI());
            } else if (method == RequestMethod.PUT) {
                request = new HttpPut(url.toURI());
            }

            request.setHeader("User-Agent", USER_AGENT);
            for (Map.Entry<String, String> entry : headers.headers().entrySet()) {
                request.setHeader(entry.getKey(), entry.getValue());
            }

            CloseableHttpResponse response = client.execute(request);

            Headers responseHeaders = Headers.empty();
            for (Header header : response.getAllHeaders()) {
                responseHeaders.header(header.getName(), header.getValue());
            }

            return new Response(Status.valueOf(response.getStatusLine().getStatusCode()), responseHeaders, Body.of(EntityUtils.toByteArray(response.getEntity())));
        } catch (URISyntaxException | IOException e) {
            e.printStackTrace();
        }

        return Response.status(Status.BAD_REQUEST).build();
    }

    private URL buildPath(String relativePath) throws IOException {
        return new URL(baseUrl + relativePath);
    }

    @Override
    public int connectTimeout() {
        return connectTimeout;
    }

    @Override
    public Gson gson() {
        return gson;
    }
}
