/*
 * 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.cloudstack.storage.image.store;

import java.util.Date;
import java.util.Map;

import javax.inject.Inject;

import com.cloud.cpu.CPU;
import com.cloud.storage.StorageManager;
import com.cloud.user.UserData;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.storage.to.TemplateObjectTO;
import com.cloud.agent.api.storage.CreateDatadiskTemplateAnswer;

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.to.DataObjectType;
import com.cloud.agent.api.to.DataTO;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage.ImageFormat;
import com.cloud.storage.Storage.TemplateType;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateStorageResourceAssoc.Status;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.utils.component.ComponentContext;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.fsm.NoTransitionException;

import org.apache.commons.lang3.StringUtils;

@SuppressWarnings("serial")
public class TemplateObject implements TemplateInfo {
    protected Logger logger = LogManager.getLogger(getClass());
    private VMTemplateVO imageVO;
    private DataStore dataStore;
    private String url;
    private String installPath; // temporarily set installPath before passing to resource for entries with empty installPath for object store migration case
    private String deployAsIsConfiguration; // Temporarily set
    @Inject
    VMTemplateDao imageDao;
    @Inject
    ObjectInDataStoreManager objectInStoreMgr;
    @Inject
    VMTemplatePoolDao templatePoolDao;
    @Inject
    TemplateDataStoreDao templateStoreDao;
    final private boolean followRedirects;

    public TemplateObject() {
        this.followRedirects = StorageManager.DataStoreDownloadFollowRedirects.value();
    }

    protected void configure(VMTemplateVO template, DataStore dataStore) {
        if (template == null) {
            String msg = String.format("Template Object is not properly initialised %s", this.toString());
            logger.warn(msg);
        }
        imageVO = template;
        this.dataStore = dataStore;
    }

    public static TemplateObject getTemplate(VMTemplateVO vo, DataStore store, String configuration) {
        TemplateObject to = ComponentContext.inject(TemplateObject.class);
        to.deployAsIsConfiguration = configuration;
        to.configure(vo, store);
        return to;
    }

    public void setSize(Long size) {
        imageVO.setSize(size);
    }

    @Override
    public VMTemplateVO getImage() {
        if (imageVO == null) {
            String msg = String.format("Template Object is not properly initialised %s", this.toString());
            logger.error(msg);
        } // somehow the nullpointer is needed : refacter needed!?!
        return imageVO;
    }

    @Override
    public DataStore getDataStore() {
        return dataStore;
    }

    @Override
    public String getUniqueName() {
        return imageVO.getUniqueName();
    }

    @Override
    public long getId() {
        return imageVO.getId();
    }

    @Override
    public State getState() {
        return imageVO.getState();
    }

    @Override
    public String getUuid() {
        return imageVO.getUuid();
    }

    @Override
    public String getUri() {
        if (url != null) {
            return url;
        }
        VMTemplateVO image = imageDao.findById(imageVO.getId());

        return image.getUrl();

    }

    @Override
    public Long getSize() {
        if (dataStore == null) {
            return imageVO.getSize();
        }
        VMTemplateVO image = imageDao.findById(imageVO.getId());
        return image.getSize();
    }

    @Override
    public long getPhysicalSize() {
        TemplateDataStoreVO templateDataStoreVO = templateStoreDao.findByTemplate(imageVO.getId(), DataStoreRole.Image);
        if (templateDataStoreVO != null) {
            return templateDataStoreVO.getPhysicalSize();
        }
        return imageVO.getSize();
    }

    @Override
    public DataObjectType getType() {
        return DataObjectType.TEMPLATE;
    }

    @Override
    public ImageFormat getFormat() {
        return imageVO.getFormat();
    }

    @Override
    public void processEvent(ObjectInDataStoreStateMachine.Event event) {
        try {
            objectInStoreMgr.update(this, event);
        } catch (NoTransitionException e) {
            throw new CloudRuntimeException("Failed to update state", e);
        } catch (ConcurrentOperationException e) {
            throw new CloudRuntimeException("Failed to update state", e);
        } finally {
            // in case of OperationFailed, expunge the entry
            if (event == ObjectInDataStoreStateMachine.Event.OperationFailed) {
                objectInStoreMgr.deleteIfNotReady(this);
            }
        }
    }

    @Override
    public void processEvent(ObjectInDataStoreStateMachine.Event event, Answer answer) {
        try {
            if (getDataStore().getRole() == DataStoreRole.Primary) {
                if (answer instanceof CopyCmdAnswer) {
                    CopyCmdAnswer cpyAnswer = (CopyCmdAnswer)answer;
                    TemplateObjectTO newTemplate = (TemplateObjectTO)cpyAnswer.getNewData();

                    String deployAsIsConfiguration = newTemplate.getDeployAsIsConfiguration();
                    VMTemplateStoragePoolVO templatePoolRef = templatePoolDao.findByPoolTemplate(getDataStore().getId(), getId(), deployAsIsConfiguration);
                    templatePoolRef.setDownloadPercent(100);

                    setTemplateSizeIfNeeded(newTemplate, templatePoolRef);

                    templatePoolRef.setDownloadState(Status.DOWNLOADED);

                    setDownloadPathIfNeeded(newTemplate, templatePoolRef);

                    setInstallPathIfNeeded(newTemplate, templatePoolRef);

                    templatePoolDao.update(templatePoolRef.getId(), templatePoolRef);
                }
            } else if (getDataStore().getRole() == DataStoreRole.Image || getDataStore().getRole() == DataStoreRole.ImageCache) {
                if (answer instanceof CopyCmdAnswer) {
                    CopyCmdAnswer cpyAnswer = (CopyCmdAnswer)answer;
                    TemplateObjectTO newTemplate = (TemplateObjectTO)cpyAnswer.getNewData();
                    TemplateDataStoreVO templateStoreRef = templateStoreDao.findByStoreTemplate(getDataStore().getId(), getId());
                    if (newTemplate.getPath() != null) {
                        templateStoreRef.setInstallPath(newTemplate.getPath());
                    }
                    templateStoreRef.setDownloadPercent(100);
                    templateStoreRef.setDownloadState(Status.DOWNLOADED);
                    templateStoreRef.setSize(newTemplate.getSize());
                    if (newTemplate.getPhysicalSize() != null) {
                        templateStoreRef.setPhysicalSize(newTemplate.getPhysicalSize());
                    }
                    templateStoreDao.update(templateStoreRef.getId(), templateStoreRef);
                    if (getDataStore().getRole() == DataStoreRole.Image) {
                        VMTemplateVO templateVO = imageDao.findById(getId());
                        if (newTemplate.getFormat() != null) {
                            templateVO.setFormat(newTemplate.getFormat());
                        }
                        if (newTemplate.getName() != null) {
                            // For template created from snapshot, template name is determine by resource code.
                            templateVO.setUniqueName(newTemplate.getName());
                        }
                        if (newTemplate.getHypervisorType() != null) {
                            templateVO.setHypervisorType(newTemplate.getHypervisorType());
                        }
                        templateVO.setSize(newTemplate.getSize());
                        imageDao.update(templateVO.getId(), templateVO);
                    }
                } else if (answer instanceof CreateDatadiskTemplateAnswer) {
                    CreateDatadiskTemplateAnswer createAnswer = (CreateDatadiskTemplateAnswer)answer;
                    TemplateObjectTO dataDiskTemplate = createAnswer.getDataDiskTemplate();
                    TemplateDataStoreVO templateStoreRef = templateStoreDao.findByStoreTemplate(getDataStore().getId(), dataDiskTemplate.getId());
                    templateStoreRef.setInstallPath(dataDiskTemplate.getPath());
                    templateStoreRef.setDownloadPercent(100);
                    templateStoreRef.setDownloadState(Status.DOWNLOADED);
                    templateStoreRef.setSize(dataDiskTemplate.getSize());
                    templateStoreRef.setPhysicalSize(dataDiskTemplate.getPhysicalSize());
                    templateStoreDao.update(templateStoreRef.getId(), templateStoreRef);
                }
            }
            objectInStoreMgr.update(this, event);
        } catch (NoTransitionException e) {
            logger.debug("failed to update state", e);
            throw new CloudRuntimeException("Failed to update state" + e.toString());
        } catch (Exception ex) {
            logger.debug("failed to process event and answer", ex);
            objectInStoreMgr.delete(this);
            throw new CloudRuntimeException("Failed to process event", ex);
        } finally {
            // in case of OperationFailed, expunge the entry
            if (event == ObjectInDataStoreStateMachine.Event.OperationFailed) {
                objectInStoreMgr.deleteIfNotReady(this);
            }
        }
    }

    /**
     * In the case of managed storage, the install path may already be specified (by the storage plug-in), so do not overwrite it.
     */
    private void setInstallPathIfNeeded(TemplateObjectTO template, VMTemplateStoragePoolVO templatePoolRef) {
        if (StringUtils.isEmpty(templatePoolRef.getInstallPath())) {
            templatePoolRef.setInstallPath(template.getPath());
        }
    }

    /**
     * In the case of managed storage, the local download path may already be specified (by the storage plug-in), so do not overwrite it.
     */
    private void setDownloadPathIfNeeded(TemplateObjectTO template, VMTemplateStoragePoolVO templatePoolRef) {
        if (StringUtils.isEmpty(templatePoolRef.getLocalDownloadPath())) {
            templatePoolRef.setLocalDownloadPath(template.getPath());
        }
    }

    /**
     *  In the case of managed storage, the template size may already be specified (by the storage plug-in), so do not overwrite it.
     */
    private void setTemplateSizeIfNeeded(TemplateObjectTO template, VMTemplateStoragePoolVO templatePoolRef) {
        if (templatePoolRef.getTemplateSize() == 0 && template.getSize() != null) {
            templatePoolRef.setTemplateSize(template.getSize());
        }
    }

    @Override
    public void incRefCount() {
        if (dataStore == null) {
            return;
        }

        if (dataStore.getRole() == DataStoreRole.Image || dataStore.getRole() == DataStoreRole.ImageCache) {
            TemplateDataStoreVO store = templateStoreDao.findByStoreTemplate(dataStore.getId(), getId());
            store.incrRefCnt();
            store.setLastUpdated(new Date());
            templateStoreDao.update(store.getId(), store);
        }
    }

    @Override
    public void decRefCount() {
        if (dataStore == null) {
            return;
        }
        if (dataStore.getRole() == DataStoreRole.Image || dataStore.getRole() == DataStoreRole.ImageCache) {
            TemplateDataStoreVO store = templateStoreDao.findByStoreTemplate(dataStore.getId(), getId());
            store.decrRefCnt();
            store.setLastUpdated(new Date());
            templateStoreDao.update(store.getId(), store);
        }
    }

    @Override
    public Long getRefCount() {
        if (dataStore == null) {
            return null;
        }
        if (dataStore.getRole() == DataStoreRole.Image || dataStore.getRole() == DataStoreRole.ImageCache) {
            TemplateDataStoreVO store = templateStoreDao.findByStoreTemplate(dataStore.getId(), getId());
            return store.getRefCnt();
        }
        return null;
    }

    @Override
    public String getDeployAsIsConfiguration() {
        return deployAsIsConfiguration;
    }

    @Override
    public Long getUserDataId() {
        return imageVO.getUserDataId();
    }

    @Override
    public UserData.UserDataOverridePolicy getUserDataOverridePolicy() {
        return imageVO.getUserDataOverridePolicy();
    }

    @Override
    public CPU.CPUArch getArch() {
        return imageVO.getArch();
    }

    @Override
    public Long getExtensionId() {
        return imageVO.getExtensionId();
    }

    @Override
    public DataTO getTO() {
        DataTO to = null;
        if (dataStore == null) {
            to = new TemplateObjectTO(this);
        } else {
            to = dataStore.getDriver().getTO(this);
            if (to == null) {
                to = new TemplateObjectTO(this);
            }
        }

        return to;
    }

    @Override
    public String getInstallPath() {
        if (installPath != null) {
            return installPath;
        }

        if (dataStore == null) {
            return null;
        }

        DataObjectInStore obj = objectInStoreMgr.findObject(this, dataStore);

        return obj != null ? obj.getInstallPath() : null;
    }

    @Override
    public boolean isDirectDownload() {
        if (this.imageVO == null) {
            return false;
        }
        return this.imageVO.isDirectDownload();
    }

    @Override
    public boolean canBeDeletedFromDataStore() {
        Status downloadStatus = Status.UNKNOWN;
        int downloadPercent = -1;
        if (getDataStore().getRole() == DataStoreRole.Primary) {
            VMTemplateStoragePoolVO templatePoolRef = templatePoolDao.findByPoolTemplate(getDataStore().getId(), getId(), null);
            if (templatePoolRef != null) {
                downloadStatus = templatePoolRef.getDownloadState();
                downloadPercent = templatePoolRef.getDownloadPercent();
            }
        } else if (dataStore.getRole() == DataStoreRole.Image || dataStore.getRole() == DataStoreRole.ImageCache) {
            TemplateDataStoreVO templateStoreRef = templateStoreDao.findByStoreTemplate(dataStore.getId(), getId());
            if (templateStoreRef != null) {
                downloadStatus = templateStoreRef.getDownloadState();
                downloadPercent = templateStoreRef.getDownloadPercent();
                templateStoreRef.getState();
            }
        }

        // Marking downloaded templates for deletion, but might skip any deletion handled for failed templates.
        // Only templates not downloaded and in error state (with no install path) cannot be deleted from the datastore, so doesn't impact last behavior for templates with other states
        if (downloadStatus == null  || downloadStatus == Status.NOT_DOWNLOADED || (downloadStatus == Status.DOWNLOAD_ERROR && downloadPercent == 0)) {
            logger.debug("Template: " + getId() + " cannot be deleted from the store: " + getDataStore().getId());
            return false;
        }

        return true;
    }

    @Override
    public boolean isDeployAsIs() {
        if (this.imageVO == null) {
            return false;
        }
        return this.imageVO.isDeployAsIs();
    }

    @Override
    public boolean isForCks() {
        return imageVO.isForCks();
    }

    public void setInstallPath(String installPath) {
        this.installPath = installPath;
    }

    @Override
    public long getAccountId() {
        return imageVO.getAccountId();
    }

    @Override
    public boolean isFeatured() {
        return imageVO.isFeatured();
    }

    @Override
    public boolean isPublicTemplate() {
        return imageVO.isPublicTemplate();
    }

    @Override
    public boolean isExtractable() {
        return imageVO.isExtractable();
    }

    @Override
    public String getName() {
        return imageVO.getName();
    }

    @Override
    public boolean isRequiresHvm() {
        return imageVO.isRequiresHvm();
    }

    @Override
    public String getDisplayText() {
        return imageVO.getDisplayText();
    }

    @Override
    public boolean isEnablePassword() {
        return imageVO.isEnablePassword();
    }

    @Override
    public boolean isEnableSshKey() {
        return imageVO.isEnableSshKey();
    }

    @Override
    public boolean isCrossZones() {
        return imageVO.isCrossZones();
    }

    @Override
    public Date getCreated() {
        return imageVO.getCreated();
    }

    @Override
    public long getGuestOSId() {
        return imageVO.getGuestOSId();
    }

    @Override
    public boolean isBootable() {
        return imageVO.isBootable();
    }

    @Override
    public TemplateType getTemplateType() {
        return imageVO.getTemplateType();
    }

    @Override
    public HypervisorType getHypervisorType() {
        return imageVO.getHypervisorType();
    }

    @Override
    public int getBits() {
        return imageVO.getBits();
    }

    @Override
    public String getUrl() {
        if (url != null) {
            return url;
        }
        return imageVO.getUrl();
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public String getChecksum() {
        return imageVO.getChecksum();
    }

    @Override
    public Long getSourceTemplateId() {
        return imageVO.getSourceTemplateId();
    }

    @Override
    public Long getParentTemplateId() {
        return imageVO.getParentTemplateId();
    }

    @Override
    public String getTemplateTag() {
        return imageVO.getTemplateTag();
    }

    @Override
    public Map<String, String> getDetails() {
        return imageVO.getDetails();
    }

    @Override
    public boolean isDynamicallyScalable() {
        return false;
    }

    @Override
    public long getDomainId() {
        return imageVO.getDomainId();
    }

    @Override
    public boolean delete() {
        if (dataStore != null) {
            return dataStore.delete(this);
        }
        return true;
    }

    @Override
    public Class<?> getEntityType() {
        return VirtualMachineTemplate.class;
    }

    @Override
    public long getUpdatedCount() {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void incrUpdatedCount() {
        // TODO Auto-generated method stub
    }

    @Override
    public Date getUpdated() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean isFollowRedirects() {
        return followRedirects;
    }

    @Override
    public String toString() {
        return String.format("TemplateObject %s",
                ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
                        this, "imageVO", "dataStore"));
    }
}
