Code/Dependencies/DotNetGraph/Core/DotElement.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Nodebox.Dependencies.DotNetGraph.Attributes;
using Nodebox.Dependencies.DotNetGraph.Compilation;
using Nodebox.Dependencies.DotNetGraph.Exceptions;

namespace Nodebox.Dependencies.DotNetGraph.Core {
    public abstract class DotElement : IDotElement {
        protected readonly Dictionary<string, IDotAttribute> Attributes = new Dictionary<string, IDotAttribute>();

        // Common attributes
        public DotLabelAttribute Label {
            get => GetAttributeOrDefault<DotLabelAttribute>("label");
            set => SetAttribute("label", value);
        }

        public DotColorAttribute FontColor {
            get => GetAttributeOrDefault<DotColorAttribute>("fontcolor");
            set => SetAttribute("fontcolor", value);
        }

        // Attribute methods
        public bool HasAttribute(string name) {
            return Attributes.ContainsKey(name);
        }

        public IDotAttribute GetAttribute(string name) {
            if (Attributes.TryGetValue(name, out var attribute))
                return attribute;
            throw new AttributeNotFoundException(name);
        }

        public T GetAttribute<T>(string name) where T : IDotAttribute {
            var attribute = GetAttribute(name);
            if (attribute is T result)
                return result;
            throw new AttributeTypeNotMatchException(name, typeof(T), attribute.GetType());
        }

        public IDotAttribute GetAttributeOrDefault(string name, IDotAttribute defaultValue = default) {
            if (Attributes.TryGetValue(name, out var attribute))
                return attribute;
            return defaultValue;
        }

        public T GetAttributeOrDefault<T>(string name, T defaultValue = default) where T : IDotAttribute {
            if (Attributes.TryGetValue(name, out var attribute)) {
                if (attribute is T result)
                    return result;
                throw new AttributeTypeNotMatchException(name, typeof(T), attribute.GetType());
            }

            return defaultValue;
        }

        public bool TryGetAttribute(string name, out IDotAttribute attribute) {
            var result = Attributes.TryGetValue(name, out var outAttribute);
            if (result) {
                attribute = outAttribute;
                return true;
            }

            attribute = default;
            return false;
        }

        public bool TryGetAttribute<T>(string name, out T attribute) where T : IDotAttribute {
            var result = TryGetAttribute(name, out var untypedAttribute);
            if (!result) {
                attribute = default;
                return false;
            }

            if (untypedAttribute is T typedAttribute) {
                attribute = typedAttribute;
                return true;
            }

            throw new AttributeTypeNotMatchException(name, typeof(T), untypedAttribute.GetType());
        }

        public void SetAttribute(string name, IDotAttribute value) {
            if (value is null)
                RemoveAttribute(name);
            else
                Attributes[name] = value;
        }

        public bool RemoveAttribute(string name) {
            return Attributes.Remove(name);
        }

        protected async Task CompileAttributesAsync(CompilationContext context) {
            foreach (var attributePair in Attributes) {
                await context.WriteIndentationAsync();
                await context.WriteAsync($"\"{attributePair.Key}\"=");
                await attributePair.Value.CompileAsync(context);
                await context.WriteLineAsync();
            }
        }

        public abstract Task CompileAsync(CompilationContext context);
    }
}