/*
 *
 * 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.qpid.server.model.adapter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.log4j.Logger;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.configuration.updater.TaskExecutor;
import org.apache.qpid.server.model.AuthenticationProvider;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.IllegalStateTransitionException;
import org.apache.qpid.server.model.LifetimePolicy;
import org.apache.qpid.server.model.PreferencesProvider;
import org.apache.qpid.server.model.State;
import org.apache.qpid.server.model.Statistics;
import org.apache.qpid.server.util.MapValueConverter;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.type.TypeReference;

public class FileSystemPreferencesProvider extends AbstractAdapter implements PreferencesProvider
{
    private static final Logger LOGGER = Logger.getLogger(FileSystemPreferencesProvider.class);
    public static String PATH = "path";
    public static final String PROVIDER_TYPE = "FileSystemPreferences";

    @SuppressWarnings("serial")
    private static final Map<String, Object> DEFAULTS = Collections.unmodifiableMap(new HashMap<String, Object>()
    {{
            put(TYPE, FileSystemPreferencesProvider.class.getSimpleName());
    }});

    @SuppressWarnings("serial")
    private static final Map<String, Type> ATTRIBUTE_TYPES = Collections.unmodifiableMap(new HashMap<String, Type>()
    {{
            put(NAME, String.class);
            put(PATH, String.class);
            put(TYPE, String.class);
    }});

    @SuppressWarnings("serial")
    private static Collection<String> AVAILABLE_ATTRIBUTES = Collections.unmodifiableList(new ArrayList<String>(
            PreferencesProvider.AVAILABLE_ATTRIBUTES)
    {{
            add(PATH);
    }});

    private final AuthenticationProvider _authenticationProvider;
    private AtomicReference<State> _state;

    private FileSystemPreferencesStore _store;

    protected FileSystemPreferencesProvider(UUID id, Map<String, Object> attributes, AuthenticationProvider authenticationProvider,
            TaskExecutor taskExecutor)
    {
        super(id, DEFAULTS, MapValueConverter.convert(attributes, ATTRIBUTE_TYPES), taskExecutor);
        State state = MapValueConverter.getEnumAttribute(State.class, STATE, attributes, State.INITIALISING);
        _state = new AtomicReference<State>(state);
        addParent(AuthenticationProvider.class, authenticationProvider);
        _authenticationProvider = authenticationProvider;
        _store = new FileSystemPreferencesStore(new File(MapValueConverter.getStringAttribute(PATH, attributes)));
    }

    @Override
    public Collection<String> getAttributeNames()
    {
        return AVAILABLE_ATTRIBUTES;
    }

    @Override
    public String getName()
    {
        return (String) getAttribute(AuthenticationProvider.NAME);
    }

    @Override
    public String setName(String currentName, String desiredName) throws IllegalStateException, AccessControlException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public State getActualState()
    {
        return _state.get();
    }

    @Override
    public boolean isDurable()
    {
        return true;
    }

    @Override
    public void setDurable(boolean durable) throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public LifetimePolicy getLifetimePolicy()
    {
        return LifetimePolicy.PERMANENT;
    }

    @Override
    public LifetimePolicy setLifetimePolicy(LifetimePolicy expected, LifetimePolicy desired) throws IllegalStateException,
            AccessControlException, IllegalArgumentException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getTimeToLive()
    {
        return 0;
    }

    @Override
    public long setTimeToLive(long expected, long desired) throws IllegalStateException, AccessControlException,
            IllegalArgumentException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public Statistics getStatistics()
    {
        return NoStatistics.getInstance();
    }

    @Override
    public <C extends ConfiguredObject> Collection<C> getChildren(Class<C> clazz)
    {
        return Collections.emptySet();
    }

    @Override
    public Object getAttribute(String name)
    {
        if (CREATED.equals(name))
        {
            // TODO
        }
        else if (DURABLE.equals(name))
        {
            return true;
        }
        else if (ID.equals(name))
        {
            return getId();
        }
        else if (LIFETIME_POLICY.equals(name))
        {
            return LifetimePolicy.PERMANENT;
        }
        else if (STATE.equals(name))
        {
            return getActualState();
        }
        else if (TIME_TO_LIVE.equals(name))
        {
            // TODO
        }
        else if (UPDATED.equals(name))
        {
            // TODO
        }
        return super.getAttribute(name);
    }

    @Override
    public boolean setState(State currentState, State desiredState) throws IllegalStateTransitionException, AccessControlException
    {
        State state = _state.get();
        if (desiredState == State.DELETED)
        {
            if ((state == State.INITIALISING || state == State.ACTIVE || state == State.STOPPED || state == State.QUIESCED || state == State.ERRORED)
                    && _state.compareAndSet(state, State.DELETED))
            {
                try
                {
                    _store.close();
                }
                finally
                {
                    _store.delete();
                    _authenticationProvider.setPreferencesProvider(null);
                }
                return true;
            }
            else
            {
                throw new IllegalStateException("Cannot delete preferences provider in state: " + state);
            }
        }
        else if (desiredState == State.ACTIVE)
        {
            if ((state == State.INITIALISING || state == State.QUIESCED || state == State.STOPPED)
                    && _state.compareAndSet(state, State.ACTIVE))
            {
                try
                {
                    _store.open();
                    return true;
                }
                catch (Exception e)
                {
                    _state.compareAndSet(State.ACTIVE, State.ERRORED);
                    Broker broker = getAuthenticationProvider().getParent(Broker.class);
                    if (broker != null && broker.isManagementMode())
                    {
                        LOGGER.warn("Failed to activate preferences provider: " + getName(), e);
                    }
                    else
                    {
                        throw new RuntimeException(e);
                    }
                }
            }
            else
            {
                throw new IllegalStateException("Cannot activate preferences provider in state: " + state);
            }
        }
        else if (desiredState == State.QUIESCED)
        {
            if (state == State.INITIALISING && _state.compareAndSet(state, State.QUIESCED))
            {
                _store.close();
                return true;
            }
        }
        else if (desiredState == State.STOPPED)
        {
            if (_state.compareAndSet(state, State.STOPPED))
            {
                _store.close();
                return true;
            }
            else
            {
                throw new IllegalStateException("Cannot stop preferences preferences in state: " + state);
            }
        }

        return false;
    }

    @Override
    public Map<String, Object> getPreferences(String userId)
    {
        return _store.getPreferences(userId);
    }

    @Override
    public Map<String, Object> setPreferences(String userId, Map<String, Object> preferences)
    {
        return _store.setPreferences(userId, preferences);
    }

    @Override
    public String[] deletePreferences(String... userIDs)
    {
        return _store.deletePreferences(userIDs);
    }

    @Override
    public Set<String> listUserIDs()
    {
        return _store.listUserIDs();
    }

    public AuthenticationProvider getAuthenticationProvider()
    {
        return _authenticationProvider;
    }

    @Override
    protected void changeAttributes(Map<String, Object> attributes)
    {
        Map<String, Object> effectiveAttributes = MapValueConverter.convert(super.generateEffectiveAttributes(attributes),
                ATTRIBUTE_TYPES);
        validateAttributes(effectiveAttributes);
        String effectivePath = (String) effectiveAttributes.get(PATH);
        String currentPath = (String) getAttribute(PATH);

        File storeFile = new File(effectivePath);
        FileSystemPreferencesStore newStore = null;
        if (!effectivePath.equals(currentPath))
        {
            if (!storeFile.exists())
            {
                throw new IllegalConfigurationException("Path to preferences file does not exist!");
            }
            newStore = new FileSystemPreferencesStore(storeFile);
            newStore.open();
        }

        try
        {
            super.changeAttributes(attributes);

            if (newStore != null)
            {
                _store.close();
                _store = newStore;
                newStore = null;
            }
        }
        finally
        {
            if (newStore != null)
            {
                newStore.close();
            }
        }
        // if provider was previously in ERRORED state then set its state to ACTIVE
        _state.compareAndSet(State.ERRORED, State.ACTIVE);
    }

    private void validateAttributes(Map<String, Object> attributes)
    {
        super.validateChangeAttributes(attributes);

        String newName = (String) attributes.get(NAME);
        String currentName = getName();
        if (!currentName.equals(newName))
        {
            throw new IllegalConfigurationException("Changing the name of preferences provider is not supported");
        }
        String newType = (String) attributes.get(TYPE);
        String currentType = (String) getAttribute(TYPE);
        if (!currentType.equals(newType))
        {
            throw new IllegalConfigurationException("Changing the type of preferences provider is not supported");
        }
        String path = (String) attributes.get(PATH);
        if (path == null || path.equals("") || !(path instanceof String))
        {
            throw new IllegalConfigurationException("Path to preferences file is not specified");
        }
    }

    public void createStoreIfNotExist()
    {
        _store.createIfNotExist();
    }

    public static class FileSystemPreferencesStore
    {
        private final ObjectMapper _objectMapper;
        private final Map<String, Map<String, Object>> _preferences;
        private File _storeFile;
        private FileLock _storeLock;
        private RandomAccessFile _storeRAF;

        public FileSystemPreferencesStore(File preferencesFile)
        {
            _storeFile = preferencesFile;
            _objectMapper = new ObjectMapper();
            _objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
            _objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
            _preferences = new TreeMap<String, Map<String, Object>>();
        }

        public void createIfNotExist()
        {
            if (!_storeFile.exists())
            {
                File parent = _storeFile.getParentFile();
                if (!parent.exists() && !parent.mkdirs())
                {
                    throw new IllegalConfigurationException("Cannot create preferences store folders");
                }
                try
                {
                    if (_storeFile.createNewFile() && !_storeFile.exists())
                    {
                        throw new IllegalConfigurationException("Preferences store file was not created:" + _storeFile.getAbsolutePath());
                    }
                }
                catch (IOException e)
                {
                    throw new IllegalConfigurationException("Cannot create preferences store file");
                }
            }
        }

        public void delete()
        {
            if (_storeFile.exists() && !_storeFile.delete())
            {
                LOGGER.warn("Failed to delete preferences provider file '" + _storeFile.getName() + "'");
            }
        }

        public void open()
        {
            if (!_storeFile.exists())
            {
                throw new IllegalConfigurationException("Preferences file does not exist");
            }

            if (_storeLock != null)
            {
                throw new IllegalStateException("Preferences store is already opened");
            }
            try
            {
                _storeRAF = new RandomAccessFile(_storeFile, "rw");
                FileChannel fileChannel = _storeRAF.getChannel();
                try
                {
                    _storeLock = fileChannel.tryLock();
                }
                catch (OverlappingFileLockException e)
                {
                    _storeLock = null;
                }
                if (_storeLock == null)
                {
                    throw new IllegalConfigurationException("Cannot get lock on store file " + _storeFile.getName()
                            + " is another instance running?");
                }
                long fileSize = fileChannel.size();
                if (fileSize > 0)
                {
                    ByteBuffer buffer = ByteBuffer.allocate((int) fileSize);
                    fileChannel.read(buffer);
                    buffer.rewind();
                    buffer.flip();
                    byte[] data = buffer.array();
                    try
                    {
                        Map<String, Map<String, Object>> preferencesMap = _objectMapper.readValue(data,
                                new TypeReference<Map<String, Map<String, Object>>>()
                                {
                                });
                        _preferences.putAll(preferencesMap);
                    }
                    catch (JsonProcessingException e)
                    {
                        throw new IllegalConfigurationException("Cannot parse preferences json in " + _storeFile.getName(), e);
                    }
                }
            }
            catch (IOException e)
            {
                throw new IllegalConfigurationException("Cannot load preferences from " + _storeFile.getName(), e);
            }
        }

        public void close()
        {
            synchronized (_preferences)
            {
                try
                {
                    if (_storeLock != null)
                    {
                        _storeLock.release();
                    }
                }
                catch (IOException e)
                {
                    LOGGER.error("Cannot release file lock for preferences file store", e);
                }
                finally
                {
                    _storeLock = null;
                    try
                    {
                        if (_storeRAF != null)
                        {
                            _storeRAF.close();
                        }
                    }
                    catch (IOException e)
                    {
                        LOGGER.error("Cannot close preferences file", e);
                    }
                    finally
                    {
                        _storeRAF = null;
                        _preferences.clear();
                    }
                }
            }
        }

        public Map<String, Object> getPreferences(String userId)
        {
            checkStoreOpened();
            Map<String, Object> userPreferences = null;
            synchronized (_preferences)
            {
                userPreferences = _preferences.get(userId);
            }
            if (userPreferences != null)
            {
                return new HashMap<String, Object>(userPreferences);
            }
            return Collections.emptyMap();
        }

        public Map<String, Object> setPreferences(String userId, Map<String, Object> preferences)
        {
            checkStoreOpened();
            Map<String, Object> userPreferences = null;
            synchronized (_preferences)
            {
                userPreferences = _preferences.get(userId);
                if (userPreferences == null)
                {
                    userPreferences = new HashMap<String, Object>(preferences);
                    _preferences.put(userId, userPreferences);
                }
                else
                {
                    userPreferences.putAll(preferences);
                }
                save();
            }
            return userPreferences;
        }

        public String[] deletePreferences(String... userIDs)
        {
            checkStoreOpened();
            Set<String> deletedUsers = new HashSet<String>();
            synchronized (_preferences)
            {
                for (String id : userIDs)
                {
                    if (_preferences.containsKey(id))
                    {
                        _preferences.remove(id);
                        deletedUsers.add(id);
                    }
                }
                if (!deletedUsers.isEmpty())
                {
                    save();
                }
            }
            return deletedUsers.toArray(new String[deletedUsers.size()]);
        }

        public Set<String> listUserIDs()
        {
            checkStoreOpened();
            synchronized (_preferences)
            {
                return Collections.unmodifiableSet(_preferences.keySet());
            }
        }

        private void save()
        {
            checkStoreOpened();
            try
            {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                _objectMapper.writeValue(baos, _preferences);
                FileChannel channel = _storeRAF.getChannel();
                long currentSize = channel.size();
                channel.position(0);
                channel.write(ByteBuffer.wrap(baos.toByteArray()));
                if (currentSize > baos.size())
                {
                    channel.truncate(baos.size());
                }
            }
            catch (IOException e)
            {
                throw new IllegalConfigurationException("Cannot store preferences", e);
            }
        }

        private void checkStoreOpened()
        {
            if (_storeLock == null)
            {
                throw new IllegalStateException("Preferences store is not opened");
            }
        }

    }
}
