protected virtual void D(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }
            if (values.Length < 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a factor. Line: {1}.", token, state.LineNumber));
            }

            if (string.Equals(values[1], "-halo", StringComparison.OrdinalIgnoreCase))
            {
                if (values.Length < 3)
                {
                    throw new InvalidDataException(string.Format("The <{0}> statement must specify a factor. Line: {1}.", token, state.LineNumber));
                }
                if (values.Length != 3)
                {
                    throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
                }

                state.Material.IsHaloDissolve = true;
                state.Material.DissolveFactor = float.Parse(values[2], CultureInfo.InvariantCulture);
            }
            else
            {
                if (values.Length != 2)
                {
                    throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
                }

                state.Material.DissolveFactor = float.Parse(values[1], CultureInfo.InvariantCulture);
            }
        }
        protected virtual void TF(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }

            state.Material.TransmissionColor = Color(state, token, values);
        }
        protected virtual void MapNS(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }

            state.Material.SpecularExponentMap = Map(state, token, values);
        }
        protected virtual MaterialMap Map(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            var mapState = new MaterialMapState()
            {
                Index      = 0,
                LineNumber = state.LineNumber,
                Map        = new MaterialMap(),
                Token      = token
            };

            while (mapState.Index < values.Length)
            {
                mapState.Index++;

                if (values.Length - mapState.Index < 1)
                {
                    throw new InvalidDataException(string.Format("The <{0}> statement must specify a filename. Line: {1}.", token, state.LineNumber));
                }

                var optionString = values[mapState.Index].ToLowerInvariant().Trim(new[] { '-' });

                if (!Enum.TryParse <MaterialMapToken>(optionString, true, out var option))
                {
                    if (!Path.HasExtension(values[mapState.Index]))
                    {
                        throw new InvalidDataException(string.Format("A filename must have an extension. Value: <{0}>. Line: {1}.", values[mapState.Index], state.LineNumber));
                    }

                    mapState.Map.FileName = values[mapState.Index];
                    mapState.Index++;

                    if (mapState.Index != values.Length)
                    {
                        throw new InvalidDataException(string.Format("The <{0}> has too many values. Line: {1}.", token, state.LineNumber));
                    }

                    continue;
                }


                if (!MapExecutors.TryGetValue(option, out var executor))
                {
                    throw new InvalidOperationException(string.Format("Unable to find an executor for token <{0}>.", option));
                }

                executor.Invoke(mapState, option, values);
            }

            return(mapState.Map);
        }
        protected virtual void Sharpness(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }
            if (values.Length < 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a sharpness value. Line: {1}.", token, state.LineNumber));
            }
            if (values.Length != 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
            }

            state.Material.Sharpness = int.Parse(values[1], CultureInfo.InvariantCulture);
        }
        protected virtual void NewMtl(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (values.Length < 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a name. Line: {1}.", token, state.LineNumber));
            }
            if (values.Length != 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
            }

            state.Material = new Material()
            {
                Name = values[1]
            };
            state.Collection.Materials.Add(state.Material);
        }
        protected virtual void MapAAt(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }
            if (values.Length < 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a value. Line: {1}.", token, state.LineNumber));
            }
            if (values.Length != 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
            }

            if (!Enum.TryParse <OnOff>(values[1], true, out var onOffValue))
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify on or off. Line: {1}.", token, state.LineNumber));
            }

            state.Material.IsAntiAliasingEnabled = onOffValue == OnOff.On;
        }
        protected virtual void Refl(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (state.Material == default)
            {
                throw new InvalidDataException(string.Format("The material name is not specified. Line: {0}", state.LineNumber));
            }
            if (values.Length < 4)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a type and a file name. Line: {1}.", token, state.LineNumber));
            }
            if (!string.Equals(values[1], "-type", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a type. Line: {1}.", token, state.LineNumber));
            }

            if (!Enum.TryParse <MaterialReflectionToken>(values[2], true, out var reflectionType))
            {
                throw new InvalidDataException(string.Format("Unable to parse <{0}> token. Line: {1}.", values[2], state.LineNumber));
            }

            switch (reflectionType)
            {
            case MaterialReflectionToken.Sphere:
            {
                state.Material.ReflectionMap.Sphere = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Top:
            {
                state.Material.ReflectionMap.CubeTop = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Bottom:
            {
                state.Material.ReflectionMap.CubeBottom = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Front:
            {
                state.Material.ReflectionMap.CubeFront = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Back:
            {
                state.Material.ReflectionMap.CubeBack = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Left:
            {
                state.Material.ReflectionMap.CubeLeft = Map(state, token, values);
                break;
            }

            case MaterialReflectionToken.Cube_Right:
            {
                state.Material.ReflectionMap.CubeRight = Map(state, token, values);
                break;
            }
            }
        }
        protected virtual MaterialColor Color(IMaterialReaderState state, MaterialToken token, string[] values)
        {
            if (values.Length < 2)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement must specify a color. Line: {1}.", token, state.LineNumber));
            }

            var color = new MaterialColor();

            int index = 1;

            if (!Enum.TryParse <MaterialColorToken>(values[1], true, out var tokenType))
            {
                tokenType = MaterialColorToken.RGB;
            }

            switch (tokenType)
            {
            case MaterialColorToken.Spectral:
            {
                index++;

                if (values.Length - index < 1)
                {
                    throw new InvalidDataException(string.Format("The <{0}> spectral statement must specify a file name. Line: {1}.", token, state.LineNumber));
                }

                if (!Path.HasExtension(values[index]))
                {
                    throw new InvalidDataException(string.Format("A filename must have an extension. Value: <{0}>. Line: {1}.", values[index], state.LineNumber));
                }

                color.SpectralFileName = values[index];
                index++;

                if (values.Length > index)
                {
                    color.SpectralFactor = float.Parse(values[index], CultureInfo.InvariantCulture);
                    index++;
                }

                break;
            }

            case MaterialColorToken.XYZ:
            {
                index++;

                if (values.Length - index < 1)
                {
                    throw new InvalidDataException(string.Format("The <{0}> xyz statement must specify a color. Line: {1}.", token, state.LineNumber));
                }

                color.UseXYZColorSpace = true;

                var xyz = new XYZ()
                {
                    X = float.Parse(values[index], CultureInfo.InvariantCulture)
                };

                index++;

                if (values.Length > index)
                {
                    if (values.Length - index < 2)
                    {
                        throw new InvalidDataException(string.Format("The <{0}> xyz statement must specify a XYZ color. Line: {1}.", token, state.LineNumber));
                    }

                    xyz.Y  = float.Parse(values[index], CultureInfo.InvariantCulture);
                    xyz.Z  = float.Parse(values[index + 1], CultureInfo.InvariantCulture);
                    index += 2;
                }
                else
                {
                    xyz.Y = xyz.X;
                    xyz.Z = xyz.X;
                }

                color.Color = xyz;
                break;
            }

            default:
            {
                var rgb = new XYZ()
                {
                    X = float.Parse(values[index], CultureInfo.InvariantCulture)
                };

                index++;

                if (values.Length > index)
                {
                    if (values.Length - index < 2)
                    {
                        throw new InvalidDataException(string.Format("The <{0}> statement must specify a RGB color. Line: {1}.", token, state.LineNumber));
                    }

                    rgb.Y  = float.Parse(values[index], CultureInfo.InvariantCulture);
                    rgb.Z  = float.Parse(values[index + 1], CultureInfo.InvariantCulture);
                    index += 2;
                }
                else
                {
                    rgb.Y = rgb.X;
                    rgb.Z = rgb.X;
                }

                color.Color = rgb;
                break;
            }
            }

            if (index != values.Length)
            {
                throw new InvalidDataException(string.Format("The <{0}> statement has too many values. Line: {1}.", token, state.LineNumber));
            }

            return(color);
        }