/*
 * 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.apache.ignite.internal.processors.hadoop.impl.fs;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.mapreduce.MRJobConfig;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.ignite.IgniteException;
import org.apache.ignite.hadoop.fs.v1.IgniteHadoopFileSystem;
import org.apache.ignite.internal.util.GridStringBuilder;
import org.apache.ignite.internal.util.typedef.F;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.net.URI;

/**
 * File system cache utility methods used by Map-Reduce tasks and jobs.
 */
public class HadoopFileSystemCacheUtils {
    /**
     * A common static factory method. Creates new HadoopLazyConcurrentMap.
     * @return a new HadoopLazyConcurrentMap.
     */
    public static HadoopLazyConcurrentMap<FsCacheKey, FileSystem> createHadoopLazyConcurrentMap() {
        return new HadoopLazyConcurrentMap<>(
            new HadoopLazyConcurrentMap.ValueFactory<FsCacheKey, FileSystem>() {
                @Override public FileSystem createValue(FsCacheKey key) throws IOException {
                    assert key != null;

                    // Explicitly disable FileSystem caching:
                    URI uri = key.uri();

                    String scheme = uri.getScheme();

                    // Copy the configuration to avoid altering the external object.
                    Configuration cfg = new Configuration(key.configuration());

                    String prop = HadoopFileSystemsUtils.disableFsCachePropertyName(scheme);

                    cfg.setBoolean(prop, true);

                    if (UserGroupInformation.getCurrentUser().getAuthenticationMethod() ==
                        UserGroupInformation.AuthenticationMethod.TOKEN)
                        return FileSystem.get(uri, cfg);
                    else {
                        try {
                            return FileSystem.get(uri, cfg, key.user());
                        }
                        catch (InterruptedException e) {
                            Thread.currentThread().interrupt();

                            throw new IOException("Failed to create file system due to interrupt.", e);
                        }
                    }
                }
            }
        );
    }

    /**
     * Gets non-null user name as per the Hadoop viewpoint.
     * @param cfg the Hadoop job configuration, may be null.
     * @return the user name, never null.
     */
    private static String getMrHadoopUser(Configuration cfg) throws IOException {
        String user = cfg.get(MRJobConfig.USER_NAME);

        if (user == null)
            user = IgniteHadoopFileSystem.getFsHadoopUser();

        return user;
    }

    /**
     * Common method to get the V1 file system in MapRed engine.
     * It gets the filesystem for the user specified in the
     * configuration with {@link MRJobConfig#USER_NAME} property.
     * The file systems are created and cached in the given map upon first request.
     *
     * @param uri The file system uri.
     * @param cfg The configuration.
     * @param map The caching map.
     * @return The file system.
     * @throws IOException On error.
     */
    public static FileSystem fileSystemForMrUserWithCaching(@Nullable URI uri, Configuration cfg,
        HadoopLazyConcurrentMap<FsCacheKey, FileSystem> map)
            throws IOException {
        assert map != null;
        assert cfg != null;

        final String usr = getMrHadoopUser(cfg);

        assert usr != null;

        if (uri == null)
            uri = FileSystem.getDefaultUri(cfg);

        final FileSystem fs;

        try {
            final FsCacheKey key = new FsCacheKey(uri, usr, cfg);

            fs = map.getOrCreate(key);
        }
        catch (IgniteException ie) {
            throw new IOException(ie);
        }

        assert fs != null;
        assert !(fs instanceof IgniteHadoopFileSystem) || F.eq(usr, ((IgniteHadoopFileSystem)fs).user());

        return fs;
    }

    /**
     * Takes Fs URI using logic similar to that used in FileSystem#get(1,2,3).
     * @param uri0 The uri.
     * @param cfg The cfg.
     * @return Correct URI.
     */
    private static URI fixUri(URI uri0, Configuration cfg) {
        if (uri0 == null)
            return FileSystem.getDefaultUri(cfg);

        String scheme = uri0.getScheme();
        String authority = uri0.getAuthority();

        if (authority == null) {
            URI dfltUri = FileSystem.getDefaultUri(cfg);

            if (scheme == null || (scheme.equals(dfltUri.getScheme()) && dfltUri.getAuthority() != null))
                return dfltUri;
        }

        return uri0;
    }

    /**
     * Note that configuration is not a part of the key.
     * It is used solely to initialize the first instance
     * that is created for the key.
     */
    public static final class FsCacheKey {
        /** */
        private final URI uri;

        /** */
        private final String usr;

        /** */
        private final String equalityKey;

        /** */
        private final Configuration cfg;

        /**
         * Constructor
         */
        public FsCacheKey(URI uri, String usr, Configuration cfg) {
            assert uri != null;
            assert usr != null;
            assert cfg != null;

            this.uri = fixUri(uri, cfg);
            this.usr = usr;
            this.cfg = cfg;

            this.equalityKey = createEqualityKey();
        }

        /**
         * Creates String key used for equality and hashing.
         */
        private String createEqualityKey() {
            GridStringBuilder sb = new GridStringBuilder("(").a(usr).a(")@");

            if (uri.getScheme() != null)
                sb.a(uri.getScheme().toLowerCase());

            sb.a("://");

            if (uri.getAuthority() != null)
                sb.a(uri.getAuthority().toLowerCase());

            return sb.toString();
        }

        /**
         * The URI.
         */
        public URI uri() {
            return uri;
        }

        /**
         * The User.
         */
        public String user() {
            return usr;
        }

        /**
         * The Configuration.
         */
        public Configuration configuration() {
            return cfg;
        }

        /** {@inheritDoc} */
        @SuppressWarnings("SimplifiableIfStatement")
        @Override public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj == null || getClass() != obj.getClass())
                return false;

            return equalityKey.equals(((FsCacheKey)obj).equalityKey);
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return equalityKey.hashCode();
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return equalityKey;
        }
    }
}