use std::fmt::{self, Display};

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::punctuated::{Pair, Punctuated};
use syn::{Ident, Token};

pub enum Name {
    Short(Short),
    Long(Long),
}

pub struct Short {
    pub hyphen: Token![-],
    pub ch: Ident,
}

pub struct Long {
    pub hyphen1: Token![-],
    pub hyphen2: Token![-],
    pub segments: Punctuated<Ident, Token![-]>,
}

impl Short {
    pub fn ch(&self) -> char {
        self.ch.to_string().chars().next().unwrap()
    }
}

impl Long {
    pub fn to_ident(&self) -> Ident {
        let span = self.segments[0].span();
        let symbol = self.to_string().replace('-', "_").to_uppercase();
        Ident::new(&symbol, span)
    }
}

impl Parse for Name {
    fn parse(input: ParseStream) -> Result<Self> {
        let hyphen1: Token![-] = input.parse()?;

        if input.fork().call(Ident::parse_any).is_ok() {
            let short = input.call(Ident::parse_any)?;
            if short.to_string().chars().count() > 1 {
                let spanned = quote!(#hyphen1 #short);
                let msg = "short flag must be a single character";
                return Err(Error::new_spanned(spanned, msg));
            }
            Ok(Name::Short(Short {
                hyphen: hyphen1,
                ch: short,
            }))
        } else if let Some(hyphen2) = input.parse()? {
            let segments = input.call(parse_long_segments)?;
            Ok(Name::Long(Long {
                hyphen1,
                hyphen2,
                segments,
            }))
        } else {
            Err(input.error("expected short (-v) or long (--verbose) flag"))
        }
    }
}

fn parse_long_segments(input: ParseStream) -> Result<Punctuated<Ident, Token![-]>> {
    let mut segments = Punctuated::new();
    let first = input.call(Ident::parse_any)?;
    segments.push_value(first);

    while input.peek(Token![-]) {
        let hyphen: Token![-] = input.parse()?;
        segments.push_punct(hyphen);
        let segment = input.call(Ident::parse_any)?;
        segments.push_value(segment);
    }

    Ok(segments)
}

impl ToTokens for Short {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.hyphen.to_tokens(tokens);
        self.ch.to_tokens(tokens);
    }
}

impl ToTokens for Long {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        self.hyphen1.to_tokens(tokens);
        self.hyphen2.to_tokens(tokens);
        self.segments.to_tokens(tokens);
    }
}

impl Display for Long {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        for pair in self.segments.pairs() {
            match pair {
                Pair::Punctuated(ident, _hyphen) => {
                    Display::fmt(ident, formatter)?;
                    formatter.write_str("-")?;
                }
                Pair::End(ident) => {
                    Display::fmt(ident, formatter)?;
                }
            }
        }
        Ok(())
    }
}
