/*
 * Copyright (c) 2016-2017, Guangshan ([email protected]) and the original author or authors.
 *
 * Licensed 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 com.mxixm.fastboot.weixin.support;

import com.mxixm.fastboot.weixin.module.media.*;
import com.mxixm.mapdb.DB;
import com.mxixm.mapdb.DBMaker;
import com.mxixm.mapdb.HTreeMap;
import com.mxixm.mapdb.Serializer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URI;

/**
 * FastBootWeixin MapDbWxMediaStore
 * <p>
 * media存储器,提供媒体文件获取,媒体文件保存,转换文件等功能
 * 数据库使用内嵌数据库,经过一天的maven仓库database embedded选型,暂时决定使用MapDB(200k,其实有700K)或者kahaDB(600k)
 * 还有一个PalDB,这些都不小,真不行了我自己实现一个好了。。。暂时先用现成的
 * MapDB最新版依赖真的太多了,不想用了,先用MapDB的老版本吧
 * <p>
 * 重要!这个store类要优化成callback方式,且做的易于扩展,现在我自己都有点看不懂这个东西了
 *
 * @author Guangshan
 * @date 2017/09/21 23:37
 * @since 0.1.2
 */
public class MapDbWxMediaStore implements InitializingBean, WxMediaStore {

    private static final Log logger = LogFactory.getLog(MethodHandles.lookup().lookupClass());

    private DB db;

    private HTreeMap<String, String> mediaIdDb;

    private HTreeMap<String, StoreEntity> mediaDb;

    private String defaultFilePath = "weixin/media/file/";

    private String defaultTempFilePath = "weixin/media/file/temp/";

    private String defaultDbPath = "weixin/media/db/store.db";

    private Long tempExpired = 3 * 24 * 60 * 60 * 1000L;

    /**
     * 整个查询的封装
     * @param mediaQuery
     * @return MediaEntity
     */
    @Override
    public MediaEntity query(MediaQuery mediaQuery) {
        if (mediaQuery.getMediaId() != null) {
            String key = mediaIdDb.get(mediaQuery.getMediaId());
            mediaQuery.setKey(key);
        }
        MediaEntity result = new MediaEntity();
        StoreEntity storeEntity = mediaDb.get(mediaQuery.getKey());
        if (!expired(mediaQuery, storeEntity)) {
            storeEntity.fillMediaEntity(result);
        }
        return result;
    }

    /**
     * 处理素材的失效
     * @return boolean 素材是否失效
     */
    private boolean expired(MediaQuery mediaQuery, StoreEntity storeEntity) {
        if (storeEntity == null) {
            return true;
        }
        if (mediaQuery.getModifiedTime() != null && storeEntity.modifiedTime < mediaQuery.getModifiedTime()) {
            mediaIdDb.remove(storeEntity.mediaId);
            mediaDb.remove(mediaQuery.getKey());
            return true;
        }
        if (Type.TEMP.equals(storeEntity.storeType)) {
            if (System.currentTimeMillis() - storeEntity.createdTime >= tempExpired) {
                mediaIdDb.remove(storeEntity.mediaId);
                mediaDb.remove(mediaQuery.getKey());
                return true;
            }
        }
        return false;
    }

    /**
     * 保存tempMedia到mediaStore
     *
     * @param mediaEntity
     */
    @Override
    public MediaEntity store(MediaEntity mediaEntity) {
        StoreEntity storeEntity = StoreEntity.builder()
                .resourcePath(mediaEntity.getResourcePath())
                .resourceUrl(mediaEntity.getResourceUrl())
                .createdTime(mediaEntity.getCreatedTime())
                .modifiedTime(mediaEntity.getModifiedTime())
                .mediaType(mediaEntity.getMediaType())
                .mediaId(mediaEntity.getMediaId())
                .mediaUrl(mediaEntity.getMediaUrl())
                .storeType(mediaEntity.getStoreType())
                .build();
        mediaDb.put(mediaEntity.getKey(), storeEntity);
        if (mediaEntity.getMediaId() != null) {
            mediaIdDb.put(mediaEntity.getMediaId(), mediaEntity.getKey());
        }
        // 每执行一个写操作,都要commit,否则强制终止程序时会导致打不开数据库文件
        db.commit();
        return mediaEntity;
    }


    /**
     * 保存Resource到持久化,这个实现中是文件
     *
     * @param mediaEntity
     * @return File
     */
    @Override
    public Resource storeResource(MediaEntity mediaEntity) throws IOException {
        if (!(mediaEntity.getResource() instanceof WxMediaResource)) {
            return null;
        }
        WxMediaResource wxMediaResource = (WxMediaResource) mediaEntity.getResource();
        if (wxMediaResource.isUrlMedia()) {
            return null;
        }
        String fileName = wxMediaResource.getFilename();
        if (fileName == null) {
            fileName = mediaEntity.getMediaId();
        }
        File file = new File(StringUtils.applyRelativePath(Type.TEMP.equals(mediaEntity.getStoreType()) ? defaultTempFilePath : defaultFilePath, fileName));
        if (file.exists()) {
            return new FileSystemResource(file);
        }
        file.createNewFile();
        file.setLastModified(System.currentTimeMillis());
        FileCopyUtils.copy(mediaEntity.getResource().getInputStream(), new FileOutputStream(file));
        mediaEntity.setResourcePath(file.getAbsolutePath());
        store(mediaEntity);
        return new FileSystemResource(file);
    }

    /**
     * 只能用来删除永久素材
     *
     * @param mediaId
     */
    @Override
    public void delete(String mediaId) {
        String key = mediaIdDb.remove(mediaId);
        mediaDb.remove(key);
        db.commit();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        File filePath = new File(defaultFilePath);
        if (!filePath.exists()) {
            filePath.mkdirs();
        }
        filePath = new File(defaultTempFilePath);
        if (!filePath.exists()) {
            filePath.mkdirs();
        }
        File dbFile = new File(defaultDbPath);
        if (!dbFile.exists()) {
            dbFile.getParentFile().mkdirs();
            dbFile.createNewFile();
        }
        db = DBMaker.newFileDB(dbFile)
                .cacheDisable()
                .asyncWriteEnable()
                .asyncWriteFlushDelay(100)
                .closeOnJvmShutdown().make();
        mediaDb = db.createHashMap("media")
                .keySerializer(Serializer.STRING)
                .counterEnable()
                .makeOrGet();
        mediaIdDb = db.createHashMap("mediaId")
                .keySerializer(Serializer.STRING)
                .counterEnable()
                .makeOrGet();
    }

    /**
     * 用于存储的实体
     */
    private static class StoreEntity implements Serializable {
        /**
         * 资源路径
         */
        private String resourcePath;

        /**
         * 资源URL
         */
        private String resourceUrl;

        /**
         * 媒体ID
         */
        private String mediaId;

        /**
         * 媒体URL
         */
        private String mediaUrl;

        /**
         * 媒体的创建时间
         */
        private Long createdTime;

        /**
         * 最后一次更新时间
         */
        private Long modifiedTime;

        /**
         * 媒体类型
         */
        private WxMedia.Type mediaType;

        /**
         * 是否是临时资源
         */
        private WxMediaStore.Type storeType;

        StoreEntity(String resourcePath, String resourceUrl, String mediaId, String mediaUrl, Long createdTime, Long modifiedTime, WxMedia.Type mediaType, WxMediaStore.Type storeType) {
            this.resourcePath = resourcePath;
            this.resourceUrl = resourceUrl;
            this.mediaId = mediaId;
            this.mediaUrl = mediaUrl;
            this.createdTime = createdTime;
            this.modifiedTime = modifiedTime;
            this.mediaType = mediaType;
            this.storeType = storeType;
        }

        void fillMediaEntity(MediaEntity mediaEntity) {
            mediaEntity.setResourcePath(resourcePath);
            mediaEntity.setResourceUrl(resourceUrl);
            mediaEntity.setMediaId(mediaId);
            mediaEntity.setMediaUrl(mediaUrl);
            mediaEntity.setCreatedTime(createdTime);
            mediaEntity.setModifiedTime(modifiedTime);
            mediaEntity.setMediaType(mediaType);
            mediaEntity.setStoreType(storeType);
            if (resourcePath != null) {
                mediaEntity.setResource(new FileSystemResource(resourcePath));
            } else if (resourceUrl != null) {
                try {
                    mediaEntity.setResource(new UrlResource(URI.create(resourceUrl)));
                } catch (MalformedURLException e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }

        public static Builder builder() {
            return new Builder();
        }

        public static class Builder {
            private String resourcePath;
            private String resourceUrl;
            private String mediaId;
            private String mediaUrl;
            private Long createdTime;
            private Long modifiedTime;
            private WxMedia.Type mediaType;
            private WxMediaStore.Type storeType;

            Builder() {
            }

            public Builder resourcePath(String resourcePath) {
                this.resourcePath = resourcePath;
                return this;
            }

            public Builder resourceUrl(String resourceUrl) {
                this.resourceUrl = resourceUrl;
                return this;
            }

            public Builder mediaId(String mediaId) {
                this.mediaId = mediaId;
                return this;
            }

            public Builder mediaUrl(String mediaUrl) {
                this.mediaUrl = mediaUrl;
                return this;
            }

            public Builder createdTime(Long createdTime) {
                this.createdTime = createdTime;
                return this;
            }

            public Builder modifiedTime(Long modifiedTime) {
                this.modifiedTime = modifiedTime;
                return this;
            }

            public Builder mediaType(WxMedia.Type mediaType) {
                this.mediaType = mediaType;
                return this;
            }

            public Builder storeType(WxMediaStore.Type storeType) {
                this.storeType = storeType;
                return this;
            }

            public StoreEntity build() {
                return new StoreEntity(resourcePath, resourceUrl, mediaId, mediaUrl, createdTime, modifiedTime, mediaType, storeType);
            }

            @Override
            public String toString() {
                return "com.mxixm.fastboot.weixin.support.MapDbWxMediaStore.StoreEntity.Builder(resourcePath=" + this.resourcePath + ", mediaId=" + this.mediaId + ", mediaUrl=" + this.mediaUrl + ", createdTime=" + this.createdTime + ", modifiedTime=" + this.modifiedTime + ", mediaType=" + this.mediaType + ")";
            }
        }
    }

}