_UniaxalMaterials = [
    "Steel02",
    "Steel01",
    "Concrete01",
    "Concrete02",
    "Pinching4",
    "ASDConcrete1D"
]


def Material(type=None, name=None, *args, **kwds):
    if type is None:
        if len(kwds) <= 2:
            type = "Elastic"
        else:
            raise ValueError("Material type must be specified when more than 2 parameters are provided.")

    if type == "Elastic":
        return _ElasticMaterial(*args, **kwds)
    elif type in _UniaxalMaterials:
        return _UniaxialMaterial(type, *args, **kwds)
    elif "J2" in type or type == "Plastic":
        return _PlasticMaterial(type, *args, **kwds)
    else:
        return _MultiaxialMaterial(type, *args, **kwds)



class _Material:
    def __init__(self, type, tag=None, **kwds):
        self.type = type
        self._mtag = tag

    def _add_to_model(self, model, tag):
        raise NotImplementedError("Material type must implement '_add_to_model' method.")

    def __getitem__(self, key):
        return getattr(self, key)


class _UniaxialMaterial(_Material):
    def __init__(self, type, *args, G=None, nu=None, **kwds):
        super().__init__(type, **kwds)
        self._args = args
        kwds = {self._normalize_parameter(k): v for k, v in kwds.items()}
        self._kwds = kwds
        if "E" in kwds:
            self.E = kwds["E"]
        
        if type in {"Concrete01", "Concrete02"}:
            if "E" not in kwds:
                if "Fc" not in kwds or "ec0" not in kwds:
                    raise ValueError("Concrete materials require either E or both Fc and ec0.")
                self.E = 2*kwds["Fc"]/kwds["ec0"]

        if G is not None and nu is not None:
            raise ValueError("Only one of Poisson's ratio (nu) or shear modulus (G) should be provided.")
        elif G is not None:
            self.G = G
            self.nu = self.E / (2 * G) - 1
        elif nu is not None:
            self.G = self.E / (2 * (1 + nu))
            self.nu = nu

    def _add_to_model(self, model, tag):
        self._mtag = tag
        kwds = {k: v for k, v in self._kwds.items() if k not in {"G", "nu"}}
        if self.type in {"Concrete01", "Concrete02"} and "E" in kwds:
            del kwds["E"]
        model._invoke_proc("uniaxialMaterial", self.type, tag, **kwds)

    def _normalize_parameter(self, name):
        if self.type in {"Concrete01", "Concrete02"}:
            if name in {"ec0", "epsc0"}:
                return "ec0"
            elif name in {"fc", "Fc"}:
                return "Fc"
        return name
        


class _ElasticMaterial(_Material):
    def __init__(self, E, nu=None, G=None, tag=None):
        super().__init__("Elastic", tag=tag)
        self.E = E
        if nu is None and G is None:
            raise ValueError("Either Poisson's ratio (nu) or shear modulus (G) must be provided.")
        elif nu is not None and G is not None:
            raise ValueError("Only one of Poisson's ratio (nu) or shear modulus (G) should be provided.")
        elif nu is not None:
            self.nu = nu
            self.G = E / (2 * (1 + nu))
        else:
            self.G = G
            self.nu = E / (2 * G) - 1
    
    def _add_to_model(self, model, tag):
        self._mtag = tag
        model.material("ElasticIsotropic", tag, E=self.E, G=self.G)


class _MultiaxialMaterial(_Material):
    def __init__(self, type, *args, **kwds):
        super().__init__(type)
        self._args = args
        self._kwds = kwds
        E = self.E = kwds.get("E", None)
        G = kwds.get("G", None)
        nu = kwds.get("nu", None)

        if nu is None and G is None:
            raise ValueError("Either Poisson's ratio (nu) or shear modulus (G) must be provided.")
        elif nu is not None and G is not None:
            raise ValueError("Only one of Poisson's ratio (nu) or shear modulus (G) should be provided.")
        elif nu is not None:
            self._v = nu
            self.G = E / (2 * (1 + nu))
        else:
            self.G  = G
            self._v = E / (2 * G) - 1

    def _add_to_model(self, model, tag):
        self._mtag = tag
        model._invoke_proc("material", self.type, tag, *self._args, **self._kwds)

class _PlasticMaterial(_Material):
    def __init__(self, type, E, nu=None, G=None, Fy=None, Hiso=None, Hkin=None, **kwds):
        super().__init__(type)
        self.E = E
        self.Fy = Fy
        self.Hiso = Hiso
        self.Hkin = Hkin

        if nu is None and G is None:
            raise ValueError("Either Poisson's ratio (nu) or shear modulus (G) must be provided.")
        elif nu is not None and G is not None:
            raise ValueError("Only one of Poisson's ratio (nu) or shear modulus (G) should be provided.")
        elif nu is not None:
            self._v = nu
            self.G = E / (2 * (1 + nu))
        else:
            self.G = G
            self._v = E / (2 * G) - 1

    
    def _add_to_model(self, model, tag):
        self._mtag = tag
        mdata = {
            "E": self.E,
            "G": self.G,
            "Fy": self.Fy,
        }
        for key in ["Hiso", "Hkin"]:
            value = getattr(self, key)
            if value is not None:
                mdata[key] = value

        model.material(self.type, tag, **mdata)
