# Copyright (c) Alibaba, Inc. and its affiliates.
import glob
import os
import os.path as osp
from typing import Any

import numpy as np
from modelscope.hub.snapshot_download import snapshot_download
from modelscope.pipelines.util import is_official_hub_path
from modelscope.utils.config import Config
from modelscope.utils.constant import (DEFAULT_MODEL_REVISION, Invoke,
                                       ModelFile, ThirdParty)
from modelscope.utils.device import create_device
from PIL import ImageFile

from easycv.utils.ms_utils import EasyCVMeta


class EasyCVPipeline(object):
    """Base pipeline for EasyCV.
    Loading configuration file of modelscope style by default,
    but it is actually use the predictor api of easycv to predict.
    So here we do some adaptation work for configuration and predict api.
    """

    def __init__(self, model: str, model_file_pattern='*.pt', *args, **kwargs):
        """
            model (str): model id on modelscope hub or local model path.
            model_file_pattern (str): model file pattern.

        """
        self.model_file_pattern = model_file_pattern

        assert isinstance(model, str)
        if osp.exists(model):
            model_dir = model
        else:
            assert is_official_hub_path(
                model), 'Only support local model path and official hub path!'
            model_dir = snapshot_download(
                model_id=model,
                revision=DEFAULT_MODEL_REVISION,
                user_agent={
                    Invoke.KEY: Invoke.PIPELINE,
                    ThirdParty.KEY: ThirdParty.EASYCV
                })

        assert osp.isdir(model_dir)
        model_files = glob.glob(
            os.path.join(model_dir, self.model_file_pattern))
        assert len(
            model_files
        ) == 1, f'Need one model file, but find {len(model_files)}: {model_files}'

        model_path = model_files[0]
        self.model_path = model_path
        self.model_dir = model_dir

        # get configuration file from source model dir
        self.config_file = os.path.join(model_dir, ModelFile.CONFIGURATION)
        assert os.path.exists(
            self.config_file
        ), f'Not find "{ModelFile.CONFIGURATION}" in model directory!'

        self.cfg = Config.from_file(self.config_file)
        if 'device' in kwargs:
            kwargs['device'] = create_device(kwargs['device'])
        if 'predictor_config' in kwargs:
            kwargs.pop('predictor_config')
        self.predict_op = self._build_predict_op(**kwargs)

    def _build_predict_op(self, **kwargs):
        """Build EasyCV predictor."""
        from easycv.predictors.builder import build_predictor

        easycv_config = self._to_easycv_config()
        pipeline_op = build_predictor(self.cfg.pipeline.predictor_config, {
            'model_path': self.model_path,
            'config_file': easycv_config,
            **kwargs
        })
        return pipeline_op

    def _to_easycv_config(self):
        """Adapt to EasyCV predictor."""
        # TODO: refine config compatibility problems

        easycv_arch = self.cfg.model.pop(EasyCVMeta.ARCH, None)
        model_cfg = self.cfg.model
        # Revert to the configuration of easycv
        if easycv_arch is not None:
            model_cfg.update(easycv_arch)

        easycv_config = Config(dict(model=model_cfg))

        reserved_keys = []
        if hasattr(self.cfg, EasyCVMeta.META):
            easycv_meta_cfg = getattr(self.cfg, EasyCVMeta.META)
            reserved_keys = easycv_meta_cfg.get(EasyCVMeta.RESERVED_KEYS, [])
            for key in reserved_keys:
                easycv_config.merge_from_dict({key: getattr(self.cfg, key)})
        if 'test_pipeline' not in reserved_keys:
            easycv_config.merge_from_dict(
                {'test_pipeline': self.cfg.dataset.val.get('pipeline', [])})

        return easycv_config

    def _is_single_inputs(self, inputs):
        if isinstance(inputs, str) or (isinstance(inputs, list)
                                       and len(inputs) == 1) or isinstance(
                                           inputs, np.ndarray) or isinstance(
                                               inputs, ImageFile.ImageFile):
            return True

        return False

    def __call__(self, inputs) -> Any:
        outputs = self.predict_op(inputs)

        if self._is_single_inputs(inputs):
            outputs = outputs[0]

        return outputs
