
package com.google.refine.extension.gdata;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsRequestInitializer;
import com.google.api.services.sheets.v4.SheetsScopes;
import edu.mit.simile.butterfly.ButterflyModule;

import com.google.refine.ProjectManager;
import com.google.refine.preference.PreferenceStore;

abstract public class GoogleAPIExtension {

    protected static final String SERVICE_APP_NAME = "OpenRefine-Google-Service";

    // For a production release, the second parameter (default value) can be set
    // for the following three properties (client_id, client_secret, and API key) to
    // the production values from the Google API console
    private static final String CLIENT_ID = System.getProperty("ext.gdata.clientid", "");
    private static final String CLIENT_SECRET = System.getProperty("ext.gdata.clientsecret", "");
    private static final String API_KEY = System.getProperty("ext.gdata.apikey", "");

    /** Global instance of the HTTP transport. */
    protected static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();

    /** Global instance of the JSON factory. */
    protected static final JsonFactory JSON_FACTORY = new GsonFactory();

    private static final String[] SCOPES = { DriveScopes.DRIVE, SheetsScopes.SPREADSHEETS };

    private static PreferenceStore prefStore = ProjectManager.singleton.getPreferenceStore();

    private static final String CONNECT_TIME_OUT_KEY = "googleConnectTimeOut";
    private static final String READ_TIME_OUT_KEY = "googleReadTimeOut";
    private static final int CONNECT_TIME_OUT_DEFAULT = 3 * 60000; // 3 minutes connect timeout
    private static final int READ_TIME_OUT_DEFAULT = 3 * 60000; // 3 minutes read timeout

    static public String getAuthorizationUrl(ButterflyModule module, HttpServletRequest request)
            throws MalformedURLException {
        String host = request.getHeader("Host");
        if (CLIENT_ID.equals("") || CLIENT_SECRET.equals("")) {
            return "https://github.com/OpenRefine/OpenRefine/wiki/Google-Extension#missing-credentials";
        }
        String authorizedUrl = makeRedirectUrl(module, request);
        String state = makeState(module, request);

        GoogleAuthorizationCodeRequestUrl url = new GoogleAuthorizationCodeRequestUrl(
                CLIENT_ID,
                authorizedUrl, // execution continues at authorized on redirect
                Arrays.asList(SCOPES));
        url.setState(state);

        return url.toString();

    }

    private static String makeState(ButterflyModule module, HttpServletRequest request) {
        String winname = request.getParameter("winname");
        String cb = request.getParameter("cb");
        String json = "{\"winname\":\"" + winname.replaceAll("\"", "\\\"")
                + "\",\"cb\":\"" + cb.replaceAll("\"", "\\\"") + "\"}";
        return new String(Base64.getEncoder().encode(json.getBytes()));
    }

    private static String makeRedirectUrl(ButterflyModule module, HttpServletRequest request)
            throws MalformedURLException {
        StringBuffer sb = new StringBuffer(module.getMountPoint().getMountPoint());
        sb.append("authorized");

        URL thisUrl = new URL(request.getRequestURL().toString());
        URL authorizedUrl = new URL(thisUrl, sb.toString());

        return authorizedUrl.toExternalForm();
    }

    static public String getTokenFromCode(ButterflyModule module, HttpServletRequest request)
            throws IOException {
        String redirectUrl = makeRedirectUrl(module, request);
        StringBuffer fullUrlBuf = request.getRequestURL();
        if (request.getQueryString() != null) {
            fullUrlBuf.append('?').append(request.getQueryString());
        }
        AuthorizationCodeResponseUrl authResponse = new AuthorizationCodeResponseUrl(fullUrlBuf.toString());
        // check for user-denied error
        if (authResponse.getError() != null) {
            // authorization denied...
        } else {
            // request access token using authResponse.getCode()...
            String code = authResponse.getCode();
            GoogleTokenResponse response = new GoogleAuthorizationCodeTokenRequest(HTTP_TRANSPORT,
                    JSON_FACTORY, CLIENT_ID, CLIENT_SECRET, code, redirectUrl).execute();
            String tokenAndExpiresInSeconds = response.getAccessToken() + "," + response.getExpiresInSeconds();
            return tokenAndExpiresInSeconds;
        }
        return null;
    }

    static public Drive getDriveService(String token) {
        Credential credential = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod()).build().setAccessToken(token);

        return new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setHttpRequestInitializer(new HttpRequestInitializer() {

            @Override
            public void initialize(HttpRequest httpRequest) throws IOException {
                credential.initialize(httpRequest);
                httpRequest.setConnectTimeout(3 * 60000);
                httpRequest.setReadTimeout(3 * 60000);
            }
        })
                .setApplicationName(SERVICE_APP_NAME).build();
    }

    static boolean isSpreadsheetURL(String url) {
        try {
            return url.contains("spreadsheet") && getSpreadsheetID(new URL(url)) != null;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    static String getSpreadsheetID(URL url) {
        return getParamValue(url, "key");
    }

    static private String getParamValue(URL url, String key) {
        String query = url.getQuery();
        if (query != null) {
            String[] parts = query.split("&");
            for (String part : parts) {
                if (part.startsWith(key + "=")) {
                    int offset = key.length() + 1;
                    String tableId = part.substring(offset);
                    return tableId;
                }
            }
        }
        return null;
    }

    /**
     * Build and return an authorized Sheets API client service.
     * 
     * @return an authorized Sheets API client service
     * @throws IOException
     */
    public static Sheets getSheetsService(String token) throws IOException {
        final Credential credential;
        if (token != null) {
            credential = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod()).build().setAccessToken(token);
        } else {
            credential = null;
        }
        int connectTimeout = getConnectTimeout();
        int readTimeout = getReadTimeout();

        return new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
                .setApplicationName(SERVICE_APP_NAME)
                .setSheetsRequestInitializer(new SheetsRequestInitializer(API_KEY))
                .setHttpRequestInitializer(new HttpRequestInitializer() {

                    @Override
                    public void initialize(HttpRequest httpRequest) throws IOException {
                        if (credential != null) {
                            credential.initialize(httpRequest);
                        }
                        httpRequest.setConnectTimeout(connectTimeout);
                        httpRequest.setReadTimeout(readTimeout);
                    }
                }).build();
    }

    private static int getConnectTimeout() {
        return prefStore.get(CONNECT_TIME_OUT_KEY) == null ? CONNECT_TIME_OUT_DEFAULT
                : Integer.parseInt((String) prefStore.get(CONNECT_TIME_OUT_KEY));
    }

    private static int getReadTimeout() {
        return prefStore.get(READ_TIME_OUT_KEY) == null ? READ_TIME_OUT_DEFAULT
                : Integer.parseInt((String) prefStore.get(READ_TIME_OUT_KEY));
    }

    public static String extractSpreadSheetId(String url)
            throws IllegalArgumentException {
        URL urlAsUrl;

        Matcher matcher = Pattern.compile("(?<=/d/).*?(?=[/?#]|$)").matcher(url);
        if (matcher.find()) {
            return matcher.group(0);
        }

        try {
            urlAsUrl = new URL(url);
            String query = urlAsUrl.getQuery();
            if (query != null) {

                String[] parts = query.split("&");

                int offset = -1;
                int numParts = 0;
                String keyOrId = "";

                for (String part : parts) {
                    if (part.startsWith("id=")) {
                        offset = ("id=").length();
                        keyOrId = part.substring(offset);
                        numParts = 4;
                        break;
                    } else if (part.startsWith("key=")) {
                        offset = ("key=").length();
                        keyOrId = part.substring(offset);
                        if (!keyOrId.isEmpty()) {
                            return keyOrId;
                        }
                        numParts = 2;
                        break;
                    }
                }

                if (offset > -1) {
                    String[] dottedParts = keyOrId.split("\\.");
                    if (dottedParts.length == numParts) {
                        return dottedParts[0] + "." + dottedParts[1];
                    }
                }
            }
        } catch (MalformedURLException e) {
            // This is not a URL, maybe it is just an id
            String[] dottedParts = url.split("\\.");

            if (dottedParts.length == 4 || dottedParts.length == 2) {
                return dottedParts[0] + "." + dottedParts[1];
            }
        }

        throw new IllegalArgumentException("Unknown URL format.");
    }
}
