/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt::Result as FmtResult;
use std::fmt::Write;

use intern::string_key::StringKey;
use itertools::Itertools;

use crate::writer::FunctionTypeAssertion;
use crate::writer::KeyValuePairProp;
use crate::writer::Prop;
use crate::writer::Writer;
use crate::writer::AST;

pub struct FlowPrinter {
    result: String,
    indentation: usize,
}

impl Write for FlowPrinter {
    fn write_str(&mut self, s: &str) -> FmtResult {
        self.result.write_str(s)
    }
}

impl Writer for FlowPrinter {
    fn into_string(self: Box<Self>) -> String {
        self.result
    }

    fn write(&mut self, ast: &AST) -> FmtResult {
        match ast {
            AST::Any => write!(&mut self.result, "any"),
            AST::Mixed => write!(&mut self.result, "mixed"),
            AST::String => write!(&mut self.result, "string"),
            AST::StringLiteral(literal) => self.write_string_literal(**literal),
            AST::OtherTypename => self.write_other_string(),
            AST::Number => write!(&mut self.result, "number"),
            AST::Boolean => write!(&mut self.result, "boolean"),
            AST::Callable(return_type) => self.write_callable(&*return_type),
            AST::Identifier(identifier) => write!(&mut self.result, "{}", identifier),
            AST::RawType(raw) => write!(&mut self.result, "{}", raw),
            AST::Union(members) => self.write_union(members),
            AST::ReadOnlyArray(of_type) => self.write_read_only_array(of_type),
            AST::Nullable(of_type) => self.write_nullable(of_type),
            AST::NonNullable(of_type) => self.write_non_nullable(of_type),
            AST::ExactObject(props) => self.write_object(props, true),
            AST::InexactObject(props) => self.write_object(props, false),
            AST::Local3DPayload(document_name, selections) => {
                self.write_local_3d_payload(*document_name, selections)
            }
            AST::FragmentReference(fragments) => self.write_fragment_references(&***fragments),
            AST::FragmentReferenceType(fragment) => {
                write!(&mut self.result, "{}$fragmentType", fragment)
            }
            AST::ReturnTypeOfFunctionWithName(function_name) => {
                self.write_return_type_of_function_with_name(*function_name)
            }
            AST::ReturnTypeOfMethodCall(object, method_name) => {
                self.write_return_type_of_method_call(object, *method_name)
            }
            AST::ActorChangePoint(selections) => self.write_actor_change_point(selections),
            AST::AssertFunctionType(FunctionTypeAssertion {
                function_name,
                arguments,
                return_type,
            }) => self.write_assert_function_type(*function_name, arguments, return_type),
            AST::GenericType { outer, inner } => self.write_generic_type(*outer, inner),
            AST::PropertyType {
                type_,
                property_name,
            } => {
                self.write(type_)?;
                write!(&mut self.result, "['{}']", property_name)
            }
        }
    }

    fn get_runtime_fragment_import(&self) -> &'static str {
        "FragmentType"
    }

    fn write_local_type(&mut self, name: &str, value: &AST) -> FmtResult {
        write!(&mut self.result, "type {} = ", name)?;
        self.write(value)?;
        writeln!(&mut self.result, ";")
    }

    fn write_export_type(&mut self, name: &str, value: &AST) -> FmtResult {
        write!(&mut self.result, "export type {} = ", name)?;
        self.write(value)?;
        writeln!(&mut self.result, ";")
    }

    fn write_import_module_default(&mut self, name: &str, from: &str) -> FmtResult {
        writeln!(&mut self.result, "import {} from \"{}\";", name, from)
    }

    fn write_import_module_named(
        &mut self,
        name: &str,
        import_as: Option<&str>,
        from: &str,
    ) -> FmtResult {
        let local_name = if let Some(import_as) = import_as {
            format!("{{{} as {}}}", name, import_as)
        } else {
            format!("{{{}}}", name)
        };
        self.write_import_module_default(&local_name, from)
    }

    fn write_import_type(&mut self, types: &[&str], from: &str) -> FmtResult {
        writeln!(
            &mut self.result,
            "import type {{ {} }} from \"{}\";",
            types.iter().format(", "),
            from
        )
    }

    fn write_import_fragment_type(&mut self, types: &[&str], from: &str) -> FmtResult {
        self.write_import_type(types, from)
    }

    fn write_export_fragment_type(&mut self, name: &str) -> FmtResult {
        writeln!(
            &mut self.result,
            "declare export opaque type {name}: FragmentType;",
            name = name
        )
    }

    fn write_export_fragment_types(
        &mut self,
        fragment_type_name_1: &str,
        fragment_type_name_2: &str,
    ) -> FmtResult {
        writeln!(
            &mut self.result,
            "export type {{ {}, {} }};",
            fragment_type_name_1, fragment_type_name_2
        )
    }

    fn write_any_type_definition(&mut self, name: &str) -> FmtResult {
        writeln!(&mut self.result, "type {} = any;", name)
    }
}

impl FlowPrinter {
    pub fn new() -> Self {
        Self {
            result: String::new(),
            indentation: 0,
        }
    }

    fn write_indentation(&mut self) -> FmtResult {
        self.result.write_str(&"  ".repeat(self.indentation))
    }

    fn write_string_literal(&mut self, literal: StringKey) -> FmtResult {
        write!(&mut self.result, "\"{}\"", literal)
    }

    fn write_other_string(&mut self) -> FmtResult {
        write!(&mut self.result, r#""%other""#)
    }

    fn write_union(&mut self, members: &[AST]) -> FmtResult {
        let mut first = true;
        for member in members {
            if first {
                first = false;
            } else {
                write!(&mut self.result, " | ")?;
            }
            self.write(member)?;
        }
        Ok(())
    }

    fn write_fragment_references(&mut self, fragments: &[StringKey]) -> FmtResult {
        let mut first = true;
        for fragment in fragments {
            if first {
                first = false;
            } else {
                write!(&mut self.result, " & ")?;
            }
            write!(&mut self.result, "{}$fragmentType", fragment)?;
        }
        Ok(())
    }

    fn write_read_only_array(&mut self, of_type: &AST) -> FmtResult {
        write!(&mut self.result, "$ReadOnlyArray<")?;
        self.write(of_type)?;
        write!(&mut self.result, ">")
    }

    fn write_non_nullable(&mut self, of_type: &AST) -> FmtResult {
        write!(&mut self.result, "$NonMaybeType<")?;
        self.write(of_type)?;
        write!(&mut self.result, ">")
    }

    fn write_nullable(&mut self, of_type: &AST) -> FmtResult {
        write!(&mut self.result, "?")?;
        match of_type {
            AST::Union(members) if members.len() > 1 => {
                write!(&mut self.result, "(")?;
                self.write(of_type)?;
                write!(&mut self.result, ")")?;
            }
            _ => {
                self.write(of_type)?;
            }
        }
        Ok(())
    }

    fn write_object(&mut self, props: &[Prop], exact: bool) -> FmtResult {
        if props.is_empty() && exact {
            write!(&mut self.result, "{{||}}")?;
            return Ok(());
        }

        if exact {
            writeln!(&mut self.result, "{{|")?;
        } else {
            writeln!(&mut self.result, "{{")?;
        }
        self.indentation += 1;

        for prop in props {
            self.write_indentation()?;
            match prop {
                Prop::Spread(spread) => {
                    write!(&mut self.result, "...")?;
                    self.write(&AST::Identifier(spread.value))?;
                    writeln!(&mut self.result, ",")?;
                    continue;
                }
                Prop::KeyValuePair(key_value_pair) => {
                    if let AST::OtherTypename = key_value_pair.value {
                        writeln!(
                            &mut self.result,
                            "// This will never be '%other', but we need some"
                        )?;
                        self.write_indentation()?;
                        writeln!(
                            &mut self.result,
                            "// value in case none of the concrete values match."
                        )?;
                        self.write_indentation()?;
                    }
                    if key_value_pair.read_only {
                        write!(&mut self.result, "+")?;
                    }
                    write!(&mut self.result, "{}", key_value_pair.key)?;
                    if key_value_pair.optional {
                        write!(&mut self.result, "?")?;
                    }
                    write!(&mut self.result, ": ")?;
                    self.write(&key_value_pair.value)?;
                    writeln!(&mut self.result, ",")?;
                }
                Prop::GetterSetterPair(getter_setter_pair) => {
                    // Write the getter
                    write!(&mut self.result, "get ")?;
                    self.write(&AST::Identifier(getter_setter_pair.key))?;
                    write!(&mut self.result, "(): ")?;
                    self.write(&getter_setter_pair.getter_return_value)?;
                    writeln!(&mut self.result, ",")?;

                    // Write the setter
                    self.write_indentation()?;
                    write!(&mut self.result, "set ")?;
                    self.write(&AST::Identifier(getter_setter_pair.key))?;
                    write!(&mut self.result, "(value: ")?;
                    self.write(&getter_setter_pair.setter_parameter)?;
                    writeln!(&mut self.result, "): void,")?;
                }
            };
        }
        if !exact {
            self.write_indentation()?;
            writeln!(&mut self.result, "...")?;
        }
        self.indentation -= 1;
        self.write_indentation()?;
        if exact {
            write!(&mut self.result, "|}}")?;
        } else {
            write!(&mut self.result, "}}")?;
        }
        Ok(())
    }

    fn write_local_3d_payload(&mut self, document_name: StringKey, selections: &AST) -> FmtResult {
        write!(&mut self.result, "Local3DPayload<\"{}\", ", document_name)?;
        self.write(selections)?;
        write!(&mut self.result, ">")?;
        Ok(())
    }

    fn write_return_type_of_function_with_name(&mut self, function_name: StringKey) -> FmtResult {
        write!(&mut self.result, "ReturnType<typeof {}>", function_name)
    }

    fn write_return_type_of_method_call(
        &mut self,
        object: &AST,
        method_name: StringKey,
    ) -> FmtResult {
        write!(&mut self.result, "ReturnType<")?;
        self.write(object)?;
        write!(&mut self.result, "[\"{}\"]>", method_name)
    }

    fn write_actor_change_point(&mut self, selections: &AST) -> FmtResult {
        write!(&mut self.result, "ActorChangePoint<")?;
        self.write(selections)?;
        write!(&mut self.result, ">")?;
        Ok(())
    }

    fn write_callable(&mut self, return_type: &AST) -> FmtResult {
        write!(&mut self.result, "() => ")?;
        self.write(return_type)
    }

    fn write_assert_function_type(
        &mut self,
        function_name: StringKey,
        arguments: &[KeyValuePairProp],
        return_type: &AST,
    ) -> FmtResult {
        writeln!(
            &mut self.result,
            "// Type assertion validating that `{}` resolver is correctly implemented.",
            function_name
        )?;
        writeln!(
            &mut self.result,
            "// A type error here indicates that the type signature of the resolver module is incorrect."
        )?;
        if arguments.is_empty() {
            write!(&mut self.result, "({}: (", function_name)?;
        } else {
            writeln!(&mut self.result, "({}: (", function_name)?;
            self.indentation += 1;
            for argument in arguments.iter() {
                self.write_indentation()?;
                write!(&mut self.result, "{}: ", argument.key)?;
                self.write(&argument.value)?;
                writeln!(&mut self.result, ",")?;
            }
            self.indentation -= 1;
        }
        write!(&mut self.result, ") => ")?;
        self.write(return_type)?;
        writeln!(&mut self.result, ");")?;

        Ok(())
    }

    fn write_generic_type(&mut self, outer: StringKey, inner: &AST) -> FmtResult {
        write!(&mut self.result, "{}<", outer)?;
        self.write(inner)?;
        write!(&mut self.result, ">")
    }
}

#[cfg(test)]
mod tests {
    use intern::string_key::Intern;

    use super::*;
    use crate::writer::ExactObject;
    use crate::writer::InexactObject;
    use crate::writer::KeyValuePairProp;
    use crate::writer::SortedASTList;

    fn print_type(ast: &AST) -> String {
        let mut printer = Box::new(FlowPrinter::new());
        printer.write(ast).unwrap();
        printer.into_string()
    }

    #[test]
    fn scalar_types() {
        assert_eq!(print_type(&AST::Any), "any".to_string());
        assert_eq!(print_type(&AST::String), "string".to_string());
        assert_eq!(print_type(&AST::Number), "number".to_string());
    }

    #[test]
    fn union_type() {
        assert_eq!(
            print_type(&AST::Union(SortedASTList::new(vec![
                AST::String,
                AST::Number
            ],))),
            "string | number".to_string()
        );
    }

    #[test]
    fn read_only_array_type() {
        assert_eq!(
            print_type(&AST::ReadOnlyArray(Box::new(AST::String))),
            "$ReadOnlyArray<string>".to_string()
        );
    }

    #[test]
    fn nullable_type() {
        assert_eq!(
            print_type(&AST::Nullable(Box::new(AST::String))),
            "?string".to_string()
        );

        assert_eq!(
            print_type(&AST::Nullable(Box::new(AST::Union(SortedASTList::new(
                vec![AST::String, AST::Number,],
            ))))),
            "?(string | number)"
        )
    }

    #[test]
    fn exact_object() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(Vec::new()))),
            r"{||}".to_string()
        );

        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "single".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                }),
            ],))),
            r"{|
  single: string,
|}"
            .to_string()
        );
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: true,
                    read_only: false,
                    value: AST::String,
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: false,
                    read_only: true,
                    value: AST::Number,
                }),
            ],))),
            r"{|
  +bar: number,
  foo?: string,
|}"
            .to_string()
        );
    }

    #[test]
    fn nested_object() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: true,
                    read_only: false,
                    value: AST::ExactObject(ExactObject::new(vec![
                        Prop::KeyValuePair(KeyValuePairProp {
                            key: "nested_foo".intern(),
                            optional: true,
                            read_only: false,
                            value: AST::String,
                        }),
                        Prop::KeyValuePair(KeyValuePairProp {
                            key: "nested_foo2".intern(),
                            optional: false,
                            read_only: true,
                            value: AST::Number,
                        }),
                    ],)),
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: false,
                    read_only: true,
                    value: AST::Number,
                }),
            ],))),
            r"{|
  +bar: number,
  foo?: {|
    nested_foo?: string,
    +nested_foo2: number,
  |},
|}"
            .to_string()
        );
    }

    #[test]
    fn inexact_object() {
        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(Vec::new()))),
            r"{
  ...
}"
            .to_string()
        );

        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "single".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                }),
            ]))),
            r"{
  single: string,
  ...
}"
            .to_string()
        );

        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: true,
                    read_only: true,
                    value: AST::Number,
                })
            ]))),
            r"{
  +bar?: number,
  foo: string,
  ...
}"
            .to_string()
        );
    }

    #[test]
    fn other_comment() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "with_comment".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::OtherTypename,
                })
            ],))),
            r#"{|
  // This will never be '%other', but we need some
  // value in case none of the concrete values match.
  with_comment: "%other",
|}"#
            .to_string()
        );
    }

    #[test]
    fn import_type() {
        let mut printer = Box::new(FlowPrinter::new());
        printer.write_import_type(&["A", "B"], "module").unwrap();
        assert_eq!(
            printer.into_string(),
            "import type { A, B } from \"module\";\n"
        );
    }

    #[test]
    fn import_module() {
        let mut printer = Box::new(FlowPrinter::new());
        printer.write_import_module_default("A", "module").unwrap();
        assert_eq!(printer.into_string(), "import A from \"module\";\n");
    }

    #[test]
    fn function_return_type() {
        assert_eq!(
            print_type(&AST::ReturnTypeOfFunctionWithName("someFunc".intern())),
            "ReturnType<typeof someFunc>".to_string()
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt::Result as FmtResult;
use std::fmt::Write;

use crate::writer::Writer;
use crate::writer::AST;

#[derive(Default)]
pub struct JavaScriptPrinter {
    result: String,
}

impl Write for JavaScriptPrinter {
    fn write_str(&mut self, s: &str) -> FmtResult {
        self.result.write_str(s)
    }
}

impl Writer for JavaScriptPrinter {
    fn into_string(self: Box<Self>) -> String {
        self.result
    }

    fn write(&mut self, _ast: &AST) -> FmtResult {
        Ok(())
    }

    fn get_runtime_fragment_import(&self) -> &'static str {
        ""
    }

    fn write_local_type(&mut self, _name: &str, _value: &AST) -> FmtResult {
        Ok(())
    }

    fn write_export_type(&mut self, _name: &str, _value: &AST) -> FmtResult {
        Ok(())
    }

    fn write_import_module_default(&mut self, _name: &str, _from: &str) -> FmtResult {
        Ok(())
    }

    fn write_import_module_named(
        &mut self,
        _name: &str,
        _alias: Option<&str>,
        _from: &str,
    ) -> FmtResult {
        Ok(())
    }

    fn write_import_type(&mut self, _types: &[&str], _from: &str) -> FmtResult {
        Ok(())
    }

    fn write_import_fragment_type(&mut self, _types: &[&str], _from: &str) -> FmtResult {
        Ok(())
    }

    fn write_export_fragment_type(&mut self, _name: &str) -> FmtResult {
        Ok(())
    }

    fn write_export_fragment_types(
        &mut self,
        _fragment_type_name_1: &str,
        _fragment_type_name_2: &str,
    ) -> FmtResult {
        Ok(())
    }

    fn write_any_type_definition(&mut self, _name: &str) -> FmtResult {
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]

mod flow;
mod javascript;
mod type_selection;
mod typegen_state;
mod typescript;
mod visit;
mod write;
mod writer;

use ::intern::string_key::Intern;
use ::intern::string_key::StringKey;
use common::DirectiveName;
use common::NamedItem;
use common::ScalarName;
use common::WithLocation;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use lazy_static::lazy_static;
use relay_config::ProjectConfig;
pub use relay_config::TypegenConfig;
pub use relay_config::TypegenLanguage;
use relay_transforms::UPDATABLE_DIRECTIVE;
use schema::SDLSchema;
pub use typegen_state::FragmentLocations;
pub use write::has_raw_response_type_directive;
use write::write_fragment_type_exports_section;
use write::write_operation_type_exports_section;
use write::write_split_operation_type_exports_section;
use write::write_validator_function;
use writer::new_writer_from_config;

static REACT_RELAY_MULTI_ACTOR: &str = "react-relay/multi-actor";
static RELAY_RUNTIME: &str = "relay-runtime";
static LOCAL_3D_PAYLOAD: &str = "Local3DPayload";
static ACTOR_CHANGE_POINT: &str = "ActorChangePoint";
pub static PROVIDED_VARIABLE_TYPE: &str = "ProvidedVariablesType";
static VALIDATOR_EXPORT_NAME: &str = "validate";
static LIVE_RESOLVERS_LIVE_STATE: &str = "LiveState";
static LIVE_RESOLVERS_EXPERIMENTAL_STORE_PATH: &str =
    "relay-runtime/store/experimental-live-resolvers/LiveResolverStore";

lazy_static! {
    static ref KEY_CLIENTID: StringKey = "__id".intern();
    pub(crate) static ref KEY_DATA: StringKey = "$data".intern();
    static ref KEY_FRAGMENT_SPREADS: StringKey = "$fragmentSpreads".intern();
    static ref KEY_UPDATABLE_FRAGMENT_SPREADS: StringKey = "$updatableFragmentSpreads".intern();
    pub(crate) static ref KEY_FRAGMENT_TYPE: StringKey = "$fragmentType".intern();
    static ref FRAGMENT_PROP_NAME: StringKey = "__fragmentPropName".intern();
    static ref FUTURE_ENUM_VALUE: StringKey = "%future added value".intern();
    static ref JS_FIELD_NAME: StringKey = "js".intern();
    static ref KEY_RAW_RESPONSE: StringKey = "rawResponse".intern();
    static ref KEY_TYPENAME: StringKey = "__typename".intern();
    static ref KEY_DATA_ID: StringKey = "DataID".intern();
    static ref KEY_NODE: StringKey = "node".intern();
    static ref KEY_NODES: StringKey = "nodes".intern();
    static ref MODULE_COMPONENT: StringKey = "__module_component".intern();
    static ref RAW_RESPONSE_TYPE_DIRECTIVE_NAME: DirectiveName =
        DirectiveName("raw_response_type".intern());
    static ref RESPONSE: StringKey = "response".intern();
    static ref TYPE_BOOLEAN: ScalarName = ScalarName("Boolean".intern());
    static ref TYPE_FLOAT: ScalarName = ScalarName("Float".intern());
    static ref TYPE_ID: ScalarName = ScalarName("ID".intern());
    static ref TYPE_INT: ScalarName = ScalarName("Int".intern());
    static ref TYPE_STRING: ScalarName = ScalarName("String".intern());
    static ref VARIABLES: StringKey = "variables".intern();
    static ref SPREAD_KEY: StringKey = "\0SPREAD".intern();
    static ref LIVE_STATE_TYPE: StringKey = "LiveState".intern();
}

/// Determines whether a generated data type is "unmasked", which controls whether
/// the generated AST objects are exact (e.g. `{| foo: Foo |}`) or inexact
/// (e.g. `{ foo: Foo, ... }`).
///
/// The $data type of a fragment definition with the `@relay(mask: false)` directive
/// is unmasked, i.e. inexact. Fragments without this directive and queries are
/// masked, i.e. exact.
///
/// # Why?
///
/// * An unmasked fragment definition is meant to be used with an unmasked fragment
///   spread, though this is not enforced.
/// * An unmasked fragment spread "inlines" the child type into the parent type.
///   This occurs in the transforms pipeline, before hitting the typegen.
/// * An unmasked child fragment can be spread in multiple different fragments.
/// * Functions/components that accept a read-out unmasked fragment will receive
///   an object with the child fragment's fields and the parent fragment's fields.
/// * These parent fields can vary, depending on where the child fragment was spread.
/// * So, the type of the child fragment must be inexact, to account for the fact
///   that we don't know the parent's fields.
/// * We could theoretically emit exact types for children that are only spread in
///   a single parent, which itself is non-masked, as in those cases, we would know
///   all of the fields that are present. However, we do not want to do this, as we
///   do not want the child component to depend (even implicitly or accidentally) on
///   the fields selected by the parent.
/// * Obviously, unmasked child fragments do receive their parents fields, hence
///   `@relay(mask: false)` is discouraged in favor of `@inline`.
#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum MaskStatus {
    Unmasked,
    Masked,
}

pub fn generate_fragment_type_exports_section(
    fragment_definition: &FragmentDefinition,
    schema: &SDLSchema,
    project_config: &ProjectConfig,
    fragment_locations: &FragmentLocations,
) -> String {
    let typegen_context = TypegenContext::new(
        schema,
        project_config,
        fragment_definition
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some(),
        fragment_definition.name.map(|x| x.0),
        fragment_locations,
        false,
    );
    let mut writer = new_writer_from_config(&project_config.typegen_config);
    write_fragment_type_exports_section(&typegen_context, fragment_definition, &mut writer)
        .unwrap();
    writer.into_string()
}

pub fn generate_named_validator_export(
    fragment_definition: &FragmentDefinition,
    schema: &SDLSchema,
    project_config: &ProjectConfig,
    fragment_locations: &FragmentLocations,
) -> String {
    let typegen_context = TypegenContext::new(
        schema,
        project_config,
        fragment_definition
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some(),
        fragment_definition.name.map(|x| x.0),
        fragment_locations,
        false,
    );
    let mut writer = new_writer_from_config(&project_config.typegen_config);
    write_validator_function(&typegen_context, fragment_definition, &mut writer).unwrap();
    let validator_function_body = writer.into_string();

    if project_config.typegen_config.eager_es_modules {
        format!("export {}", validator_function_body)
    } else {
        format!(
            "module.exports.{} = {};",
            VALIDATOR_EXPORT_NAME, validator_function_body
        )
    }
}

pub fn generate_operation_type_exports_section(
    typegen_operation: &OperationDefinition,
    normalization_operation: &OperationDefinition,
    schema: &SDLSchema,
    project_config: &ProjectConfig,
    fragment_locations: &FragmentLocations,
) -> String {
    let typegen_context = TypegenContext::new(
        schema,
        project_config,
        typegen_operation
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some(),
        WithLocation::new(
            typegen_operation.name.location,
            typegen_operation.name.item.0,
        ),
        fragment_locations,
        false,
    );
    let mut writer = new_writer_from_config(&project_config.typegen_config);
    write_operation_type_exports_section(
        &typegen_context,
        typegen_operation,
        normalization_operation,
        &mut writer,
    )
    .unwrap();
    writer.into_string()
}

pub fn generate_split_operation_type_exports_section(
    typegen_operation: &OperationDefinition,
    normalization_operation: &OperationDefinition,
    schema: &SDLSchema,
    project_config: &ProjectConfig,
    fragment_locations: &FragmentLocations,
    no_optional_fields_in_raw_response_type: bool,
) -> String {
    let typegen_context = TypegenContext::new(
        schema,
        project_config,
        typegen_operation
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some(),
        WithLocation::new(
            typegen_operation.name.location,
            typegen_operation.name.item.0,
        ),
        fragment_locations,
        no_optional_fields_in_raw_response_type,
    );
    let mut writer = new_writer_from_config(&project_config.typegen_config);

    write_split_operation_type_exports_section(
        &typegen_context,
        typegen_operation,
        normalization_operation,
        &mut writer,
    )
    .unwrap();
    writer.into_string()
}

/// An immutable grab bag of configuration, etc. for type generation.
/// A new `TypegenContext` is created for each operation, fragment, and so on.
struct TypegenContext<'a> {
    schema: &'a SDLSchema,
    project_config: &'a ProjectConfig,
    fragment_locations: &'a FragmentLocations,
    has_unified_output: bool,
    generating_updatable_types: bool,
    definition_source_location: WithLocation<StringKey>,
    // All keys in raw response should be required
    no_optional_fields_in_raw_response_type: bool,
}

impl<'a> TypegenContext<'a> {
    fn new(
        schema: &'a SDLSchema,
        project_config: &'a ProjectConfig,
        generating_updatable_types: bool,
        definition_source_location: WithLocation<StringKey>,
        fragment_locations: &'a FragmentLocations,
        no_optional_fields_in_raw_response_type: bool,
    ) -> Self {
        Self {
            schema,
            project_config,
            fragment_locations,
            has_unified_output: project_config.output.is_some(),
            generating_updatable_types,
            definition_source_location,
            no_optional_fields_in_raw_response_type,
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt::Result as FmtResult;
use std::sync::Arc;

use common::InputObjectName;
use common::Location;
use fnv::FnvHashMap;
use fnv::FnvHashSet;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use indexmap::IndexMap;
use intern::string_key::StringKey;
use intern::Lookup;
use schema::EnumID;
use schema::SDLSchema;
use schema::Schema;

use crate::writer::ExactObject;
use crate::writer::Writer;
use crate::writer::AST;
use crate::KEY_DATA_ID;
use crate::LIVE_RESOLVERS_EXPERIMENTAL_STORE_PATH;
use crate::LIVE_RESOLVERS_LIVE_STATE;
use crate::LOCAL_3D_PAYLOAD;
use crate::RELAY_RUNTIME;

/// A struct that is mutated as we iterate through an operation/fragment and
/// contains information about whether and how to write import types.
#[derive(Default)]
pub(crate) struct RuntimeImports {
    pub(crate) local_3d_payload_type: bool,
    pub(crate) generic_fragment_type: bool,
    pub(crate) resolver_live_state_type: bool,
    pub(crate) data_id_type: bool,
}

impl RuntimeImports {
    pub(crate) fn write_runtime_imports(&self, writer: &mut Box<dyn Writer>) -> FmtResult {
        if self.resolver_live_state_type {
            writer.write_import_type(
                &[LIVE_RESOLVERS_LIVE_STATE],
                LIVE_RESOLVERS_EXPERIMENTAL_STORE_PATH,
            )?;
        }

        let mut runtime_import_types = vec![];
        if self.generic_fragment_type {
            runtime_import_types.push(writer.get_runtime_fragment_import())
        }
        if self.local_3d_payload_type {
            runtime_import_types.push(LOCAL_3D_PAYLOAD)
        }
        if self.data_id_type {
            runtime_import_types.push(KEY_DATA_ID.lookup());
        }
        if !runtime_import_types.is_empty() {
            writer.write_import_type(&runtime_import_types, RELAY_RUNTIME)
        } else {
            Ok(())
        }
    }
}

/// An enum used to prevent redundantly processing input objects.
pub(crate) enum GeneratedInputObject {
    Pending,
    Resolved(ExactObject),
}

impl GeneratedInputObject {
    pub(crate) fn unwrap_resolved_type(self) -> ExactObject {
        match self {
            GeneratedInputObject::Pending => panic!("Unexpected pending type"),
            GeneratedInputObject::Resolved(exact_object) => exact_object,
        }
    }
}

pub(crate) type InputObjectTypes = IndexMap<InputObjectName, GeneratedInputObject>;

/// Because EncounteredEnums is passed around everywhere, we use a newtype
/// to make it easy to track.
#[derive(Default)]
pub(crate) struct EncounteredEnums(pub(crate) FnvHashSet<EnumID>);

impl EncounteredEnums {
    pub(crate) fn into_sorted_vec(self, schema: &SDLSchema) -> Vec<EnumID> {
        let mut sorted_vec_of_enum_ids: Vec<_> = self.0.into_iter().collect();
        sorted_vec_of_enum_ids.sort_by_key(|enum_id| schema.enum_(*enum_id).name);
        sorted_vec_of_enum_ids
    }
}

#[derive(Default)]
pub(crate) struct MatchFields(pub(crate) IndexMap<StringKey, AST>);

#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) enum EncounteredFragment {
    Spread(FragmentDefinitionName),
    Key(FragmentDefinitionName),
    Data(FragmentDefinitionName),
}

/// This is a map FragmentName => Fragment Location
/// We use the location of the fragment to generate a correct
/// path to its generated artifact, in case we need to
/// reference it in another generated artifact.
/// This is used in non-haste setups that do not have a single
/// directory for generated artifacts.
pub struct FragmentLocations(pub FnvHashMap<FragmentDefinitionName, Location>);

impl FragmentLocations {
    pub fn new<'a>(fragments: impl Iterator<Item = &'a Arc<FragmentDefinition>>) -> Self {
        Self(
            fragments
                .map(|fragment| (fragment.name.item, fragment.name.location))
                .collect::<_>(),
        )
    }

    pub fn location(&self, fragment_name: &FragmentDefinitionName) -> Option<Location> {
        self.0.get(fragment_name).copied()
    }
}

#[derive(Default)]
pub(crate) struct EncounteredFragments(pub(crate) FnvHashSet<EncounteredFragment>);

pub(crate) struct ImportedResolver {
    pub resolver_name: ImportedResolverName,
    pub resolver_type: AST,
    pub import_path: StringKey,
}

#[derive(Clone, Copy)]
pub(crate) enum ImportedResolverName {
    Default(StringKey),
    Named {
        name: StringKey,
        import_as: StringKey,
    },
}

#[derive(Default)]
pub(crate) struct ImportedResolvers(pub(crate) IndexMap<StringKey, ImportedResolver>);

#[derive(Default)]
pub(crate) struct ImportedRawResponseTypes(pub(crate) IndexMap<StringKey, Option<Location>>);

/// Have we encountered an actor change? Use an enum for bookkeeping, since it
/// will be passed around in many places.
pub(crate) enum ActorChangeStatus {
    HasActorChange,
    NoActorChange,
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fmt::Result as FmtResult;
use std::fmt::Write;

use intern::string_key::Intern;
use intern::string_key::StringKey;
use itertools::Itertools;
use relay_config::TypegenConfig;

use crate::writer::Prop;
use crate::writer::SortedASTList;
use crate::writer::SortedStringKeyList;
use crate::writer::StringLiteral;
use crate::writer::Writer;
use crate::writer::AST;
use crate::KEY_DATA;
use crate::KEY_FRAGMENT_SPREADS;
use crate::KEY_FRAGMENT_TYPE;

pub struct TypeScriptPrinter {
    result: String,
    use_import_type_syntax: bool,
    indentation: usize,
}

impl Write for TypeScriptPrinter {
    fn write_str(&mut self, s: &str) -> FmtResult {
        self.result.write_str(s)
    }
}

impl Writer for TypeScriptPrinter {
    fn into_string(self: Box<Self>) -> String {
        self.result
    }

    fn get_runtime_fragment_import(&self) -> &'static str {
        "FragmentRefs"
    }

    fn write(&mut self, ast: &AST) -> FmtResult {
        match ast {
            AST::Any => write!(&mut self.result, "any"),
            AST::Mixed => write!(&mut self.result, "unknown"),
            AST::String => write!(&mut self.result, "string"),
            AST::StringLiteral(literal) => self.write_string_literal(**literal),
            AST::OtherTypename => self.write_other_string(),
            AST::Number => write!(&mut self.result, "number"),
            AST::Boolean => write!(&mut self.result, "boolean"),
            AST::Callable(return_type) => self.write_callable(&*return_type),
            AST::Identifier(identifier) => write!(&mut self.result, "{}", identifier),
            AST::RawType(raw) => write!(&mut self.result, "{}", raw),
            AST::Union(members) => self.write_union(members),
            AST::ReadOnlyArray(of_type) => self.write_read_only_array(of_type),
            AST::Nullable(of_type) => self.write_nullable(of_type),
            AST::NonNullable(of_type) => self.write_non_nullable(of_type),
            AST::ExactObject(object) => self.write_object(object),
            AST::InexactObject(object) => self.write_object(object),
            AST::Local3DPayload(document_name, selections) => {
                self.write_local_3d_payload(*document_name, selections)
            }
            AST::FragmentReference(fragments) => self.write_fragment_references(fragments),
            AST::FragmentReferenceType(fragment) => self.write_fragment_references_type(*fragment),
            AST::ReturnTypeOfFunctionWithName(function_name) => {
                self.write_return_type_of_function_with_name(*function_name)
            }
            AST::ActorChangePoint(_) => {
                panic!("ActorChangePoint is not supported yet in Typescript")
            }
            AST::ReturnTypeOfMethodCall(object, method_name) => {
                self.write_return_type_of_method_call(object, *method_name)
            }
            AST::AssertFunctionType(_) => {
                // TODO: Implement type generation for typescript
                Ok(())
            }
            AST::GenericType { outer, inner } => self.write_generic_type(*outer, inner),
            AST::PropertyType {
                type_,
                property_name,
            } => {
                self.write(type_)?;
                write!(&mut self.result, "['{}']", property_name)
            }
        }
    }

    fn write_local_type(&mut self, name: &str, value: &AST) -> FmtResult {
        write!(&mut self.result, "type {} = ", name)?;
        self.write(value)?;
        writeln!(&mut self.result, ";")
    }

    fn write_export_type(&mut self, name: &str, value: &AST) -> FmtResult {
        write!(&mut self.result, "export type {} = ", name)?;
        self.write(value)?;
        writeln!(&mut self.result, ";")
    }

    fn write_import_module_default(&mut self, name: &str, from: &str) -> FmtResult {
        let from_without_extension = from.strip_suffix(".ts").unwrap_or(from);
        writeln!(
            &mut self.result,
            "import {} from \"{}\";",
            name, from_without_extension
        )
    }

    fn write_import_module_named(
        &mut self,
        name: &str,
        import_as: Option<&str>,
        from: &str,
    ) -> FmtResult {
        let import_type = if let Some(import_as) = import_as {
            format!("{} as {}", name, import_as)
        } else {
            name.to_string()
        };
        self.write_import_type(&[&import_type], from)
    }

    fn write_import_type(&mut self, types: &[&str], from: &str) -> FmtResult {
        let from_without_extension = from.strip_suffix(".ts").unwrap_or(from);
        writeln!(
            &mut self.result,
            "import {}{{ {} }} from \"{}\";",
            if self.use_import_type_syntax {
                "type "
            } else {
                ""
            },
            types.iter().format(", "),
            from_without_extension
        )
    }

    // In TypeScript, we don't need to import "any" fragment types, these are unused.
    fn write_any_type_definition(&mut self, _name: &str) -> FmtResult {
        Ok(())
    }

    // In TypeScript, we don't export & import fragments. We just use the generic FragmentRefs type instead.
    fn write_import_fragment_type(&mut self, _types: &[&str], _from: &str) -> FmtResult {
        Ok(())
    }

    fn write_export_fragment_type(&mut self, _name: &str) -> FmtResult {
        Ok(())
    }

    fn write_export_fragment_types(
        &mut self,
        _fragment_type_name_1: &str,
        _fragment_type_name_2: &str,
    ) -> FmtResult {
        Ok(())
    }
}

impl TypeScriptPrinter {
    pub fn new(config: &TypegenConfig) -> Self {
        Self {
            result: String::new(),
            indentation: 0,
            use_import_type_syntax: config.use_import_type_syntax,
        }
    }

    fn write_indentation(&mut self) -> FmtResult {
        self.result.write_str(&"  ".repeat(self.indentation))
    }

    fn write_string_literal(&mut self, literal: StringKey) -> FmtResult {
        write!(&mut self.result, "\"{}\"", literal)
    }

    fn write_other_string(&mut self) -> FmtResult {
        write!(&mut self.result, r#""%other""#)
    }

    fn write_union(&mut self, members: &[AST]) -> FmtResult {
        let mut first = true;
        for member in members {
            if first {
                first = false;
            } else {
                write!(&mut self.result, " | ")?;
            }
            self.write(member)?;
        }
        Ok(())
    }

    fn write_read_only_array(&mut self, of_type: &AST) -> FmtResult {
        write!(&mut self.result, "ReadonlyArray<")?;
        self.write(of_type)?;
        write!(&mut self.result, ">")
    }

    fn write_non_nullable(&mut self, of_type: &AST) -> FmtResult {
        write!(&mut self.result, "NonNullable<")?;
        self.write(of_type)?;
        write!(&mut self.result, ">")
    }

    fn write_nullable(&mut self, of_type: &AST) -> FmtResult {
        let null_type = AST::RawType("null".intern());
        if let AST::Union(members) = of_type {
            let mut new_members = Vec::with_capacity(members.len() + 1);
            new_members.extend_from_slice(members);
            new_members.push(null_type);
            self.write_union(&*new_members)?;
        } else {
            self.write_union(&*vec![of_type.clone(), null_type])?;
        }
        Ok(())
    }

    fn write_object(&mut self, props: &[Prop]) -> FmtResult {
        if props.is_empty() {
            write!(&mut self.result, "{{}}")?;
            return Ok(());
        }

        // Replication of babel printer oddity: objects only containing a spread
        // are missing a newline.
        if props.len() == 1 {
            if let Prop::Spread(_) = props[0] {
                write!(&mut self.result, "{{}}")?;
                return Ok(());
            }
        }

        writeln!(&mut self.result, "{{")?;
        self.indentation += 1;

        for prop in props {
            match prop {
                Prop::Spread(_) => continue,
                Prop::KeyValuePair(key_value_pair) => {
                    self.write_indentation()?;
                    if let AST::OtherTypename = key_value_pair.value {
                        writeln!(
                            &mut self.result,
                            "// This will never be '%other', but we need some"
                        )?;
                        self.write_indentation()?;
                        writeln!(
                            &mut self.result,
                            "// value in case none of the concrete values match."
                        )?;
                        self.write_indentation()?;
                    }
                    if key_value_pair.read_only {
                        write!(&mut self.result, "readonly ")?;
                    }
                    if key_value_pair.key == *KEY_FRAGMENT_SPREADS
                        || key_value_pair.key == *KEY_FRAGMENT_TYPE
                        || key_value_pair.key == *KEY_DATA
                    {
                        write!(&mut self.result, "\" {}\"", key_value_pair.key)?;
                    } else {
                        write!(&mut self.result, "{}", key_value_pair.key)?;
                    }
                    if key_value_pair.optional {
                        write!(&mut self.result, "?")?;
                    }
                    write!(&mut self.result, ": ")?;
                    self.write(&key_value_pair.value)?;
                    writeln!(&mut self.result, ";")?;
                }
                Prop::GetterSetterPair(_) => {
                    panic!(
                        "Getters and setters with different types are not implemented in typescript. See https://github.com/microsoft/TypeScript/issues/43662"
                    );
                }
            }
        }
        self.indentation -= 1;
        self.write_indentation()?;
        write!(&mut self.result, "}}")?;
        Ok(())
    }

    fn write_local_3d_payload(&mut self, document_name: StringKey, selections: &AST) -> FmtResult {
        write!(&mut self.result, "Local3DPayload<\"{}\", ", document_name)?;
        self.write(selections)?;
        write!(&mut self.result, ">")?;
        Ok(())
    }

    fn write_fragment_references(&mut self, fragments: &SortedStringKeyList) -> FmtResult {
        write!(&mut self.result, "FragmentRefs<")?;
        self.write(&AST::Union(SortedASTList::new(
            fragments
                .iter()
                .map(|key| AST::StringLiteral(StringLiteral(*key)))
                .collect(),
        )))?;
        write!(&mut self.result, ">")
    }

    fn write_fragment_references_type(&mut self, fragment: StringKey) -> FmtResult {
        self.write(&AST::StringLiteral(StringLiteral(fragment)))
    }

    fn write_return_type_of_function_with_name(&mut self, function_name: StringKey) -> FmtResult {
        write!(&mut self.result, "ReturnType<typeof {}>", function_name)
    }

    fn write_return_type_of_method_call(
        &mut self,
        object: &AST,
        method_name: StringKey,
    ) -> FmtResult {
        write!(&mut self.result, "ReturnType<")?;
        self.write(object)?;
        write!(&mut self.result, "[\"{}\"]>", method_name)
    }

    fn write_callable(&mut self, return_type: &AST) -> FmtResult {
        write!(&mut self.result, "() => ")?;
        self.write(return_type)
    }

    fn write_generic_type(&mut self, outer: StringKey, inner: &AST) -> FmtResult {
        write!(&mut self.result, "{}<", outer)?;
        self.write(inner)?;
        write!(&mut self.result, ">")
    }
}

#[cfg(test)]
mod tests {
    use intern::string_key::Intern;

    use super::*;
    use crate::writer::ExactObject;
    use crate::writer::InexactObject;
    use crate::writer::KeyValuePairProp;
    use crate::writer::SortedASTList;

    fn print_type(ast: &AST) -> String {
        print_type_with_config(ast)
    }

    fn print_type_with_config(ast: &AST) -> String {
        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer.write(ast).unwrap();
        printer.into_string()
    }

    #[test]
    fn scalar_types() {
        assert_eq!(print_type(&AST::Any), "any".to_string());
        assert_eq!(print_type(&AST::String), "string".to_string());
        assert_eq!(print_type(&AST::Number), "number".to_string());
    }

    #[test]
    fn union_type() {
        assert_eq!(
            print_type(&AST::Union(SortedASTList::new(vec![
                AST::String,
                AST::Number
            ]))),
            "string | number".to_string()
        );
    }

    #[test]
    fn read_only_array_type() {
        assert_eq!(
            print_type(&AST::ReadOnlyArray(Box::new(AST::String))),
            "ReadonlyArray<string>".to_string()
        );
    }

    #[test]
    fn nullable_type() {
        assert_eq!(
            print_type(&AST::Nullable(Box::new(AST::String))),
            "string | null".to_string()
        );

        assert_eq!(
            print_type(&AST::Nullable(Box::new(AST::Union(SortedASTList::new(
                vec![AST::String, AST::Number],
            ))))),
            "string | number | null"
        )
    }

    #[test]
    fn exact_object() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(Vec::new()))),
            r"{}".to_string()
        );

        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "single".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                })
            ]))),
            r"{
  single: string;
}"
            .to_string()
        );
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: true,
                    read_only: false,
                    value: AST::String,
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: false,
                    read_only: true,
                    value: AST::Number,
                }),
            ]))),
            r"{
  readonly bar: number;
  foo?: string;
}"
            .to_string()
        );
    }

    #[test]
    fn nested_object() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: true,
                    read_only: false,
                    value: AST::ExactObject(ExactObject::new(vec![
                        Prop::KeyValuePair(KeyValuePairProp {
                            key: "nested_foo".intern(),
                            optional: true,
                            read_only: false,
                            value: AST::String,
                        }),
                        Prop::KeyValuePair(KeyValuePairProp {
                            key: "nested_foo2".intern(),
                            optional: false,
                            read_only: true,
                            value: AST::Number,
                        }),
                    ])),
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: false,
                    read_only: true,
                    value: AST::Number,
                }),
            ]))),
            r"{
  readonly bar: number;
  foo?: {
    nested_foo?: string;
    readonly nested_foo2: number;
  };
}"
            .to_string()
        );
    }

    #[test]
    fn inexact_object() {
        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(Vec::new()))),
            "{}".to_string()
        );

        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "single".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                }),
            ]))),
            r"{
  single: string;
}"
            .to_string()
        );

        assert_eq!(
            print_type(&AST::InexactObject(InexactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "foo".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::String,
                }),
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "bar".intern(),
                    optional: true,
                    read_only: true,
                    value: AST::Number,
                })
            ]))),
            r"{
  readonly bar?: number;
  foo: string;
}"
            .to_string()
        );
    }

    #[test]
    fn other_comment() {
        assert_eq!(
            print_type(&AST::ExactObject(ExactObject::new(vec![
                Prop::KeyValuePair(KeyValuePairProp {
                    key: "with_comment".intern(),
                    optional: false,
                    read_only: false,
                    value: AST::OtherTypename,
                }),
            ]))),
            r#"{
  // This will never be '%other', but we need some
  // value in case none of the concrete values match.
  with_comment: "%other";
}"#
            .to_string()
        );
    }

    #[test]
    fn import_type() {
        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer.write_import_type(&["A", "B"], "module").unwrap();
        assert_eq!(printer.into_string(), "import { A, B } from \"module\";\n");

        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer.write_import_type(&["A", "B"], "module.ts").unwrap();
        assert_eq!(printer.into_string(), "import { A, B } from \"module\";\n");

        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer
            .write_import_type(&["A", "B"], "../../../module.ts.ts")
            .unwrap();
        assert_eq!(
            printer.into_string(),
            "import { A, B } from \"../../../module.ts\";\n"
        );

        let mut printer = Box::new(TypeScriptPrinter::new(&TypegenConfig {
            use_import_type_syntax: true,
            ..Default::default()
        }));
        printer.write_import_type(&["C"], "./foo").unwrap();
        assert_eq!(printer.into_string(), "import type { C } from \"./foo\";\n");
    }

    #[test]
    fn import_module() {
        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer.write_import_module_default("A", "module").unwrap();
        assert_eq!(printer.into_string(), "import A from \"module\";\n");

        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer
            .write_import_module_default("A", "../../module.ts")
            .unwrap();
        assert_eq!(printer.into_string(), "import A from \"../../module\";\n");

        let mut printer = Box::new(TypeScriptPrinter::new(&Default::default()));
        printer
            .write_import_module_default("A", "../../module.ts.ts")
            .unwrap();
        assert_eq!(
            printer.into_string(),
            "import A from \"../../module.ts\";\n"
        );
    }

    #[test]
    fn function_return_type() {
        assert_eq!(
            print_type(&AST::ReturnTypeOfFunctionWithName("someFunc".intern(),)),
            "ReturnType<typeof someFunc>".to_string()
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use ::intern::string_key::Intern;
use ::intern::string_key::StringKey;
use graphql_ir::FragmentDefinitionName;
use indexmap::IndexMap;
use relay_config::SchemaConfig;
use relay_transforms::TypeConditionInfo;
use schema::Type;
use schema::TypeReference;

use crate::writer::AST;
use crate::JS_FIELD_NAME;
use crate::KEY_CLIENTID;
use crate::KEY_TYPENAME;
use crate::SPREAD_KEY;

#[derive(Debug, Clone)]
pub(crate) enum TypeSelection {
    RawResponseFragmentSpread(RawResponseFragmentSpread),
    ModuleDirective(ModuleDirective),
    LinkedField(TypeSelectionLinkedField),
    ScalarField(TypeSelectionScalarField),
    InlineFragment(TypeSelectionInlineFragment),
    FragmentSpread(TypeSelectionFragmentSpread),
}

impl TypeSelection {
    pub(crate) fn set_concrete_type(&mut self, type_: Type) {
        match self {
            TypeSelection::LinkedField(l) => l.concrete_type = Some(type_),
            TypeSelection::ScalarField(s) => s.concrete_type = Some(type_),
            TypeSelection::InlineFragment(f) => f.concrete_type = Some(type_),
            TypeSelection::FragmentSpread(f) => f.concrete_type = Some(type_),
            TypeSelection::ModuleDirective(m) => m.concrete_type = Some(type_),
            TypeSelection::RawResponseFragmentSpread(f) => f.concrete_type = Some(type_),
        }
    }

    pub(crate) fn set_conditional(&mut self, conditional: bool) {
        match self {
            TypeSelection::LinkedField(l) => l.conditional = conditional,
            TypeSelection::ScalarField(s) => s.conditional = conditional,
            TypeSelection::InlineFragment(f) => f.conditional = conditional,
            TypeSelection::FragmentSpread(f) => f.conditional = conditional,
            TypeSelection::ModuleDirective(m) => m.conditional = conditional,
            TypeSelection::RawResponseFragmentSpread(f) => f.conditional = conditional,
        }
    }

    pub(crate) fn is_conditional(&self) -> bool {
        match self {
            TypeSelection::LinkedField(l) => l.conditional,
            TypeSelection::ScalarField(s) => s.conditional,
            TypeSelection::FragmentSpread(f) => f.conditional,
            TypeSelection::InlineFragment(f) => f.conditional,
            TypeSelection::ModuleDirective(m) => m.conditional,
            TypeSelection::RawResponseFragmentSpread(f) => f.conditional,
        }
    }

    pub(crate) fn get_enclosing_concrete_type(&self) -> Option<Type> {
        match self {
            TypeSelection::LinkedField(l) => l.concrete_type,
            TypeSelection::ScalarField(s) => s.concrete_type,
            TypeSelection::FragmentSpread(f) => f.concrete_type,
            TypeSelection::InlineFragment(f) => f.concrete_type,
            TypeSelection::ModuleDirective(m) => m.concrete_type,
            TypeSelection::RawResponseFragmentSpread(f) => f.concrete_type,
        }
    }

    pub(crate) fn is_typename(&self) -> bool {
        matches!(
            self,
            TypeSelection::ScalarField(TypeSelectionScalarField {
                special_field: Some(ScalarFieldSpecialSchemaField::TypeName),
                ..
            }),
        )
    }

    pub(crate) fn is_js_field(&self) -> bool {
        matches!(
            self,
            TypeSelection::ScalarField(TypeSelectionScalarField {
                special_field: Some(ScalarFieldSpecialSchemaField::JS),
                ..
            }),
        )
    }

    pub(crate) fn get_field_name_or_alias(&self) -> Option<StringKey> {
        match self {
            TypeSelection::LinkedField(l) => Some(l.field_name_or_alias),
            TypeSelection::ScalarField(s) => Some(s.field_name_or_alias),
            _ => None,
        }
    }

    pub(crate) fn get_string_key(&self) -> StringKey {
        match self {
            TypeSelection::LinkedField(l) => l.field_name_or_alias,
            TypeSelection::ScalarField(s) => s.field_name_or_alias,
            TypeSelection::FragmentSpread(i) => format!("__fragments_{}", i.fragment_name).intern(),
            TypeSelection::InlineFragment(i) => format!("__fragments_{}", i.fragment_name).intern(),
            TypeSelection::ModuleDirective(md) => md.fragment_name.0,
            TypeSelection::RawResponseFragmentSpread(_) => *SPREAD_KEY,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct RawResponseFragmentSpread {
    pub(crate) value: StringKey,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone)]
pub(crate) struct ModuleDirective {
    pub(crate) fragment_name: FragmentDefinitionName,
    pub(crate) document_name: StringKey,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone)]
pub(crate) struct TypeSelectionLinkedField {
    pub(crate) field_name_or_alias: StringKey,
    pub(crate) node_type: TypeReference<Type>,
    pub(crate) node_selections: TypeSelectionMap,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone)]
pub(crate) struct TypeSelectionScalarField {
    pub(crate) field_name_or_alias: StringKey,
    pub(crate) special_field: Option<ScalarFieldSpecialSchemaField>,
    pub(crate) value: AST,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone)]
pub(crate) struct TypeSelectionInlineFragment {
    pub(crate) fragment_name: FragmentDefinitionName,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone)]
pub(crate) struct TypeSelectionFragmentSpread {
    pub(crate) fragment_name: FragmentDefinitionName,
    pub(crate) conditional: bool,
    pub(crate) concrete_type: Option<Type>,
    // Why are we using TypeSelectionInfo instead of re-using concrete_type?
    // Because concrete_type is poorly named and does not refer to the concrete
    // type of the fragment spread.
    pub(crate) type_condition_info: Option<TypeConditionInfo>,
    pub(crate) is_updatable_fragment_spread: bool,
}

pub(crate) type TypeSelectionMap = IndexMap<TypeSelectionKey, TypeSelection>;

#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)]
pub(crate) struct TypeSelectionKey {
    pub(crate) key: StringKey,
    pub(crate) concrete_type: Option<Type>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScalarFieldSpecialSchemaField {
    JS,
    TypeName,
    Id,
    ClientId,
}

impl ScalarFieldSpecialSchemaField {
    pub(crate) fn from_schema_name(key: StringKey, schema_config: &SchemaConfig) -> Option<Self> {
        if key == *JS_FIELD_NAME {
            Some(ScalarFieldSpecialSchemaField::JS)
        } else if key == *KEY_TYPENAME {
            Some(ScalarFieldSpecialSchemaField::TypeName)
        } else if key == *KEY_CLIENTID {
            Some(ScalarFieldSpecialSchemaField::ClientId)
        } else if key == schema_config.node_interface_id_field {
            Some(ScalarFieldSpecialSchemaField::Id)
        } else {
            None
        }
    }
}